chore: clear mechanical lint warnings (Phase 1) (#12198)

* chore: clear mechanical lint warnings

Drops 44 oxlint warnings (559 → 515) by fixing easy mechanical rules
across the codebase: no-useless-escape, no-duplicate-type-constituents,
no-redundant-type-constituents, no-unused-expressions,
no-meaningless-void-operator, require-array-sort-compare, await-thenable.

* chore: drop callback parameter from useCallback deps

The `open` argument is a parameter of the callback, not a closed-over
variable, so it doesn't belong in the deps array.

* chore: promote cleared lint rules to errors

Promotes the rules cleared in this PR from warn to error so future
violations fail the lint:

- no-unused-expressions
- typescript/await-thenable
- typescript/no-duplicate-type-constituents
- typescript/no-meaningless-void-operator
- typescript/require-array-sort-compare

Removes the override that suppressed no-useless-escape on source
files (the global rule is already error) and fixes the 21 escape
violations that this exposed in regex character classes and template
literals.

* chore: address PR review feedback

- usePinnedDocuments: simplify UrlId to plain string instead of the
  intersection trick.
- PlantUML embed: move - to end of character class so it's a literal
  hyphen rather than a range operator.
- checkboxes: type token params as Token | undefined to match the
  actual call sites that pass tokens[index - 2] etc.
This commit is contained in:
Tom Moor
2026-04-28 20:00:03 -04:00
committed by GitHub
parent cd9e79b1f1
commit adbffc0734
31 changed files with 68 additions and 75 deletions
+5 -1
View File
@@ -73,9 +73,13 @@
"eqeqeq": "error",
"curly": "error",
"no-console": "error",
"no-unused-expressions": "error",
"arrow-body-style": ["error", "as-needed"],
"no-useless-escape": "off",
"react/react-in-jsx-scope": "off",
"typescript/await-thenable": "error",
"typescript/no-duplicate-type-constituents": "error",
"typescript/no-meaningless-void-operator": "error",
"typescript/require-array-sort-compare": "error",
"react/self-closing-comp": [
"error",
{
+1 -1
View File
@@ -47,7 +47,7 @@ export const ContextMenu = observer(
onClose?.();
}
},
[open, onOpen, onClose]
[onOpen, onClose]
);
const enablePointerEvents = React.useCallback(() => {
@@ -143,7 +143,7 @@ export const Suggestions = observer(
);
React.useEffect(() => {
void fetchUsersByQuery(query);
fetchUsersByQuery(query);
}, [query, fetchUsersByQuery]);
function getListItemProps(suggestion: User | Group) {
+9 -18
View File
@@ -72,8 +72,7 @@ type ContentProps = React.ComponentPropsWithoutRef<
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Content>;
const MenuContent = React.forwardRef<
| React.ElementRef<typeof DropdownMenuPrimitive.Content>
| React.ElementRef<typeof ContextMenuPrimitive.Content>,
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
ContentProps
>((props, ref) => {
const { variant } = useMenuContext();
@@ -120,8 +119,7 @@ type SubMenuTriggerProps = BaseItemProps &
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubTrigger>;
const SubMenuTrigger = React.forwardRef<
| React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>
| React.ElementRef<typeof ContextMenuPrimitive.SubTrigger>,
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
SubMenuTriggerProps
>((props, ref) => {
const { variant } = useMenuContext();
@@ -150,8 +148,7 @@ type SubMenuContentProps = React.ComponentPropsWithoutRef<
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubContent>;
const SubMenuContent = React.forwardRef<
| React.ElementRef<typeof DropdownMenuPrimitive.SubContent>
| React.ElementRef<typeof ContextMenuPrimitive.SubContent>,
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
SubMenuContentProps
>((props, ref) => {
const { variant } = useMenuContext();
@@ -203,8 +200,7 @@ type MenuGroupProps = {
>;
const MenuGroup = React.forwardRef<
| React.ElementRef<typeof DropdownMenuPrimitive.Group>
| React.ElementRef<typeof ContextMenuPrimitive.Group>,
React.ElementRef<typeof DropdownMenuPrimitive.Group>,
MenuGroupProps
>((props, ref) => {
const { variant } = useMenuContext();
@@ -275,8 +271,7 @@ type MenuButtonProps = BaseItemProps & {
>;
const MenuButton = React.forwardRef<
| React.ElementRef<typeof DropdownMenuPrimitive.Item>
| React.ElementRef<typeof ContextMenuPrimitive.Item>,
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
MenuButtonProps
>((props, ref) => {
const { variant } = useMenuContext();
@@ -338,8 +333,7 @@ type MenuInternalLinkProps = BaseItemProps & {
>;
const MenuInternalLink = React.forwardRef<
| React.ElementRef<typeof DropdownMenuPrimitive.Item>
| React.ElementRef<typeof ContextMenuPrimitive.Item>,
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
MenuInternalLinkProps
>((props, ref) => {
const { variant } = useMenuContext();
@@ -375,8 +369,7 @@ type MenuExternalLinkProps = BaseItemProps & {
>;
const MenuExternalLink = React.forwardRef<
| React.ElementRef<typeof DropdownMenuPrimitive.Item>
| React.ElementRef<typeof ContextMenuPrimitive.Item>,
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
MenuExternalLinkProps
>((props, ref) => {
const { variant } = useMenuContext();
@@ -409,8 +402,7 @@ type MenuSeparatorProps = React.ComponentPropsWithoutRef<
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Separator>;
const MenuSeparator = React.forwardRef<
| React.ElementRef<typeof DropdownMenuPrimitive.Separator>
| React.ElementRef<typeof ContextMenuPrimitive.Separator>,
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
MenuSeparatorProps
>((props, ref) => {
const { variant } = useMenuContext();
@@ -434,8 +426,7 @@ type MenuLabelProps = React.ComponentPropsWithoutRef<
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Label>;
const MenuLabel = React.forwardRef<
| React.ElementRef<typeof DropdownMenuPrimitive.Label>
| React.ElementRef<typeof ContextMenuPrimitive.Label>,
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
MenuLabelProps
>((props, ref) => {
const { variant } = useMenuContext();
+10 -2
View File
@@ -144,7 +144,11 @@ const LinkEditor: React.FC<Props> = ({
if (selectedIndex >= 0 && results[selectedIndex]) {
const selectedDoc = results[selectedIndex];
!mark ? addLink(selectedDoc.url) : updateLink(selectedDoc.url);
if (!mark) {
addLink(selectedDoc.url);
} else {
updateLink(selectedDoc.url);
}
} else if (!trimmedQuery) {
removeLink();
} else if (!mark) {
@@ -238,7 +242,11 @@ const LinkEditor: React.FC<Props> = ({
{results.map((doc, index) => (
<SuggestionsMenuItem
onClick={() => {
!mark ? addLink(doc.path) : updateLink(doc.path);
if (!mark) {
addLink(doc.path);
} else {
updateLink(doc.path);
}
}}
onPointerMove={() => setSelectedIndex(index)}
selected={index === selectedIndex}
+3 -9
View File
@@ -3,7 +3,7 @@ import { InputRule } from "@shared/editor/lib/InputRule";
const rightArrow = new InputRule(/->$/, "→");
// Note that the suppression of pipe here prevents conflict with table creation rule.
const emdash = new InputRule(/(?:^|[^\|])(--\s)$/, "— ");
const emdash = new InputRule(/(?:^|[^|])(--\s)$/, "— ");
const oneHalf = new InputRule(/(?:^|\s)(1\/2)$/, "½");
const threeQuarters = new InputRule(/(?:^|\s)(3\/4)$/, "¾");
const copyright = new InputRule(/\(c\)$/, "©️");
@@ -12,17 +12,11 @@ const trademarked = new InputRule(/\(tm\)$/, "™️");
const ellipsis = new InputRule(/\.\.\.$/, "…");
// Double quotes
const openDoubleQuote = new InputRule(
/(?:^|[\s\{\[\(\<'"\u2018\u201C])(")$/,
"“"
);
const openDoubleQuote = new InputRule(/(?:^|[\s{[(<'"\u2018\u201C])(")$/, "“");
const closeDoubleQuote = new InputRule(/^(?!.*`)[\s\S]*(")$/, "”");
// Single quotes
const openSingleQuote = new InputRule(
/(?:^|[\s\{\[\(\<'"\u2018\u201C])(')$/,
""
);
const openSingleQuote = new InputRule(/(?:^|[\s{[(<'"\u2018\u201C])(')$/, "");
const closeSingleQuote = new InputRule(/^(?!.*`)[\s\S]*(')$/, "");
export default class SmartText extends Extension {
+1 -1
View File
@@ -27,7 +27,7 @@ export default class Suggestion extends Extension {
: `(?:${triggers.map(escapeRegExp).join("|")})`;
this.openRegex = new RegExp(
`(?:^|\\s|\\(|[\\p{Script=Han}\\p{Script=Hiragana}\\p{Script=Katakana}\\p{Script=Hangul}])${triggerPattern}(${`[\\p{L}\/\\p{M}\\d${
`(?:^|\\s|\\(|[\\p{Script=Han}\\p{Script=Hiragana}\\p{Script=Katakana}\\p{Script=Hangul}])${triggerPattern}(${`[\\p{L}/\\p{M}\\d${
this.options.allowSpaces ? "\\s{1}" : ""
}\\.\\-_]+`})${this.options.requireSearchTerm ? "" : "?"}$`,
"u"
+1 -1
View File
@@ -31,7 +31,7 @@ const DEFAULT_LIMIT = 10;
* @returns
*/
export default function usePaginatedRequest<T = unknown>(
requestFn: (params?: PaginationParams | undefined) => Promise<T[]>,
requestFn: (params?: PaginationParams) => Promise<T[]>,
params: PaginationParams = {}
): RequestResponse<T> {
const [data, setData] = useState<T[]>();
+1 -1
View File
@@ -2,7 +2,7 @@ import { useEffect } from "react";
import usePersistedState from "~/hooks/usePersistedState";
import useStores from "./useStores";
type UrlId = "home" | string;
type UrlId = string;
export const pinsCacheKey = (urlId: UrlId) => `pins-${urlId}`;
+1 -1
View File
@@ -282,7 +282,7 @@ export function useDocumentSave({
titleRef.current = value;
document.title = value;
updateIsDirtyRef.current();
void autosave();
autosave();
},
[document, autosave]
);
+1 -1
View File
@@ -42,7 +42,7 @@ export class OAuthScopeHelper {
return t("Write all data");
}
const [namespace, method] = scope.replace("/api/", "").split(/[:\.]/g);
const [namespace, method] = scope.replace("/api/", "").split(/[:.]/g);
const readableMethod =
methodToReadable[method as keyof typeof methodToReadable] ?? method;
if (!readableMethod) {
+3 -1
View File
@@ -43,7 +43,9 @@ const LoadingState = observer(function LoadingState() {
ui.addActiveModel(template);
}
return () => {
template && ui.removeActiveModel(template);
if (template) {
ui.removeActiveModel(template);
}
};
}, [template, ui]);
+1 -1
View File
@@ -53,7 +53,7 @@ export default class PinsStore extends Store<Pin> {
}
@action
fetchPage = async (params?: FetchParams | undefined): Promise<Pin[]> => {
fetchPage = async (params?: FetchParams): Promise<Pin[]> => {
this.isFetching = true;
try {
+1 -3
View File
@@ -12,9 +12,7 @@ export default class StarsStore extends Store<Star> {
}
@action
fetchPage = async (
params?: PaginationParams | undefined
): Promise<Star[]> => {
fetchPage = async (params?: PaginationParams): Promise<Star[]> => {
this.isFetching = true;
try {
+1 -3
View File
@@ -19,9 +19,7 @@ export default class UserMembershipsStore extends Store<UserMembership> {
}
@action
fetchPage = async (
params?: PaginationParams | undefined
): Promise<UserMembership[]> => {
fetchPage = async (params?: PaginationParams): Promise<UserMembership[]> => {
this.isFetching = true;
try {
+1 -1
View File
@@ -401,7 +401,7 @@ export default abstract class Store<T extends Model> {
@action
fetchPage = async (
params?: FetchPageParams | undefined
params?: FetchPageParams
): Promise<PaginatedResponse<T>> => {
if (!this.actions.includes(RPCAction.List)) {
throw new Error(`Cannot list ${this.modelName}`);
+1 -1
View File
@@ -285,7 +285,7 @@ class ApiClient {
post = <T = any>(
path: string,
data?: JSONObject | FormData | undefined,
data?: JSONObject | FormData,
options?: FetchOptions
): Promise<T> => {
if (data instanceof FormData) {
+1 -1
View File
@@ -54,7 +54,7 @@ class ApiKey extends ParanoidModel<
name: string;
/** A list of scopes that this API key has access to */
@Matches(/[\/\.\w\s]*/, {
@Matches(/[/.\w\s]*/, {
each: true,
})
@Column(DataType.ARRAY(DataType.STRING))
+1 -1
View File
@@ -83,7 +83,7 @@ class OAuthAuthentication extends ParanoidModel<
grantId: string | null;
/** A list of scopes that this authentication has access to */
@Matches(/[\/\.\w\s]*/, {
@Matches(/[/.\w\s]*/, {
each: true,
})
@Column(DataType.ARRAY(DataType.STRING))
@@ -57,7 +57,7 @@ class OAuthAuthorizationCode extends IdModel<
grantId: string | null;
/** A list of scopes that this authorization code has access to */
@Matches(/[\/\.\w\s]*/, {
@Matches(/[/.\w\s]*/, {
each: true,
})
@Column(DataType.ARRAY(DataType.STRING))
+1 -1
View File
@@ -155,7 +155,7 @@ export default class ImportJSONTask extends ImportTask {
}
if (Object.values(item.attachments).length) {
await mapAttachments(item.attachments);
mapAttachments(item.attachments);
}
}
+2 -2
View File
@@ -228,11 +228,11 @@ export default class ImportMarkdownZipTask extends ImportTask {
document.text = document.text
.replace(new RegExp(escapeRegExp(encodedPath), "g"), reference)
.replace(
new RegExp(`\\\.?/?${escapeRegExp(normalizedAttachmentPath)}`, "g"),
new RegExp(`\\.?/?${escapeRegExp(normalizedAttachmentPath)}`, "g"),
reference
)
.replace(
new RegExp(`\\\.?/?${escapeRegExp(genericNormalizedPath)}`, "g"),
new RegExp(`\\.?/?${escapeRegExp(genericNormalizedPath)}`, "g"),
reference
);
}
+3 -3
View File
@@ -397,9 +397,9 @@ describe("#comments.list", () => {
expect(res.status).toEqual(200);
expect(body.data.length).toEqual(2);
expect([body.data[0].id, body.data[1].id].sort()).toEqual(
[comment1.id, comment2.id].sort()
);
expect(
[body.data[0].id, body.data[1].id].sort((a, b) => a.localeCompare(b))
).toEqual([comment1.id, comment2.id].sort((a, b) => a.localeCompare(b)));
expect(body.policies.length).toEqual(2);
expect(body.policies[0].abilities.read).toBeTruthy();
expect(body.policies[1].abilities.read).toBeTruthy();
+1 -1
View File
@@ -48,7 +48,7 @@ describe("#ValidateKey.sanitize", () => {
const uuid1 = randomUUID();
const uuid2 = randomUUID();
expect(
ValidateKey.sanitize(`public/${uuid1}/${uuid2}/~\.\u0000\malicious_key`)
ValidateKey.sanitize(`public/${uuid1}/${uuid2}/~.\u0000malicious_key`)
).toEqual(`public/${uuid1}/${uuid2}/~.malicious_key`);
});
+4 -6
View File
@@ -183,9 +183,7 @@ const embeds: EmbedDescriptor[] = [
id: "canva",
title: "Canva",
keywords: "design",
regexMatch: [
/^https:\/\/(?:www\.)?canva\.com\/design\/([\/a-zA-Z0-9_\-]*)$/,
],
regexMatch: [/^https:\/\/(?:www\.)?canva\.com\/design\/([/a-zA-Z0-9_-]*)$/],
transformMatch: (matches: RegExpMatchArray) => {
const input = matches.input ?? matches[0];
@@ -634,7 +632,7 @@ const embeds: EmbedDescriptor[] = [
id: "tella",
title: "Tella",
keywords: "video",
regexMatch: [/^https?:\/\/(?:www\.)?tella\.tv\/video\/([^\/]+)(?:.*)?$/],
regexMatch: [/^https?:\/\/(?:www\.)?tella\.tv\/video\/([^/]+)(?:.*)?$/],
transformMatch: (matches: RegExpMatchArray) =>
`https://www.tella.tv/video/${matches[1]}/embed?b=0&title=1&a=0&loop=0&t=0&muted=0&wt=1`,
icon: <Img src="/images/tella.png" alt="Tella" />,
@@ -719,7 +717,7 @@ const embeds: EmbedDescriptor[] = [
title: "YouTube",
keywords: "google video",
regexMatch: [
/(?:https?:\/\/)?(?:www\.)?youtu\.?be(?:\.com)?\/?.*(?:watch|embed)?(?:.*v=|v\/|\/)([a-zA-Z0-9_-]{11})([\&\?](.*))?$/i,
/(?:https?:\/\/)?(?:www\.)?youtu\.?be(?:\.com)?\/?.*(?:watch|embed)?(?:.*v=|v\/|\/)([a-zA-Z0-9_-]{11})([&?](.*))?$/i,
],
icon: <Img src="/images/youtube.png" alt="YouTube" />,
component: YouTube,
@@ -729,7 +727,7 @@ const embeds: EmbedDescriptor[] = [
title: "Plant UML",
keywords: "plant plantuml uml",
regexMatch: [
/(?:https?:\/\/)?(?:www\.)?editor\.plantuml\.com\/uml\/([a-zA-Z0-9\-_]+)([\&\?].*)?$/i,
/(?:https?:\/\/)?(?:www\.)?editor\.plantuml\.com\/uml\/([a-zA-Z0-9_-]+)([&?].*)?$/i,
],
icon: <Img src="/images/plantuml.png" alt="PlantUml" />,
component: PlantUmlDiagrams,
+1 -1
View File
@@ -14,7 +14,7 @@ function safeSlugify(text: string) {
const slug = `h-${escape(
slugify(text, {
remove: /[!"#$%&'\.()*+,\/:;<=>?@\[\]\\^_`{|}~]/g,
remove: /[!"#$%&'.()*+,/:;<=>?@[\]\\^_`{|}~]/g,
lower: true,
})
)}`;
+3 -3
View File
@@ -25,11 +25,11 @@ this is code
});
test("returns true for latex fence", () => {
expect(isMarkdown(`\$i\$`)).toBe(true);
expect(isMarkdown(`$i$`)).toBe(true);
expect(
isMarkdown(`\$0.00
isMarkdown(`$0.00
random content
\$1.00`)
$1.00`)
).toBe(false);
});
+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 | void) {
function matches(token: Token | undefined) {
return token && token.content.match(CHECKBOX_REGEX);
}
function isInline(token: Token | void): boolean {
function isInline(token: Token | undefined): boolean {
return !!token && token.type === "inline";
}
function isParagraph(token: Token | void): boolean {
function isParagraph(token: Token | undefined): boolean {
return !!token && token.type === "paragraph_open";
}
function isListItem(token: Token | void): boolean {
function isListItem(token: Token | undefined): 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.
+1 -1
View File
@@ -4,7 +4,7 @@ import { full as emojiPlugin } from "markdown-it-emoji";
import { isUUID } from "validator";
import { nameToEmoji } from "../lib/emoji";
type Options = MarkdownIt.Options & {
type Options = {
emoji: boolean;
};
+2 -2
View File
@@ -46,8 +46,8 @@ export default class AuthenticationHelper {
const [namespace, method] = resource.split(".");
return scopes.some((scope) => {
const [scopeNamespace, scopeMethod] = scope.match(/[:\.]/g)
? scope.replace("/api/", "").split(/[:\.]/g)
const [scopeNamespace, scopeMethod] = scope.match(/[:.]/g)
? scope.replace("/api/", "").split(/[:.]/g)
: ["*", scope];
const isRouteScope = scope.startsWith("/api/");
+1 -1
View File
@@ -82,7 +82,7 @@ export function isCurrency(value: string): boolean {
}
// Remove digits, separators, whitespace, and negative indicators
remaining = remaining.replace(/[\d.,\s()\-]/g, "");
remaining = remaining.replace(/[\d.,\s()-]/g, "");
// If anything remains, it's not a valid currency
return remaining.length === 0;