From 706cddafb0ceeb952c6fa2a1262457987fde0c8a Mon Sep 17 00:00:00 2001 From: "Timo K." Date: Tue, 7 Jul 2026 19:34:03 +0200 Subject: [PATCH] Add lint rule to make sure future child loggers are setup correctly --- .oxlintrc.json | 1 + eslint/NoTopLevelLoggerGetChild.js | 92 +++++++++++++++++++++++++++++ eslint/index.js | 1 + src/livekit/MatrixAudioRenderer.tsx | 6 +- 4 files changed, 97 insertions(+), 3 deletions(-) create mode 100644 eslint/NoTopLevelLoggerGetChild.js diff --git a/.oxlintrc.json b/.oxlintrc.json index 819e54396..d06f2a473 100644 --- a/.oxlintrc.json +++ b/.oxlintrc.json @@ -34,6 +34,7 @@ "/*\nCopyright %%CURRENT_YEAR%% Element Creations Ltd.\n\nSPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial\nPlease see LICENSE in the repository root for full details.\n*/\n\n" ], "element-call/no-observablescope-leak": "error", + "element-call/no-top-level-logger-get-child": "error", "jsdoc/empty-tags": "error", "jsdoc/check-property-names": "error", "jsdoc/require-param-description": "warn", diff --git a/eslint/NoTopLevelLoggerGetChild.js b/eslint/NoTopLevelLoggerGetChild.js new file mode 100644 index 000000000..324115d9e --- /dev/null +++ b/eslint/NoTopLevelLoggerGetChild.js @@ -0,0 +1,92 @@ +/* +Copyright 2026 Element Creations Ltd. + +SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +Please see LICENSE in the repository root for full details. +*/ + +import { ESLintUtils } from "@typescript-eslint/utils"; + +/** + * Node types that introduce a new non-module scope. A getChild() call nested + * inside any of these is considered "not at the top level". + */ +const FUNCTION_OR_CLASS_TYPES = new Set([ + "FunctionDeclaration", + "FunctionExpression", + "ArrowFunctionExpression", + "ClassBody", +]); + +const rule = ESLintUtils.RuleCreator( + () => "https://github.com/element-hq/element-call", +)({ + name: "no-top-level-logger-get-child", + meta: { + type: "problem", + docs: { + description: + "Disallow calling logger.getChild() at the top level of a module." + + "`getChild` has to be called after the rageshake logger `init()`." + + "If it is called at the top level the child logger will never be setup for rageshakes.", + }, + messages: { + noTopLevelGetChild: + "Do not call logger.getChild() at the top level of a module; move it inside a function or class instead that gets called after rageshake logger `init()` is called.", + }, + schema: [], + }, + create(context) { + // Tracks the local binding names that refer to the logger imported from + // 'matrix-js-sdk/lib/logger', e.g. both `logger` and `rootLogger` in: + // import { logger } from "matrix-js-sdk/lib/logger"; + // import { logger as rootLogger } from "matrix-js-sdk/lib/logger"; + const loggerNames = new Set(); + + return { + ImportDeclaration(node) { + if (node.source.value !== "matrix-js-sdk/lib/logger") return; + for (const specifier of node.specifiers) { + if ( + specifier.type === "ImportSpecifier" && + specifier.imported.name === "logger" + ) { + loggerNames.add(specifier.local.name); + } + } + }, + + CallExpression(node) { + // Must be a non-computed member expression call: something.getChild(...) + if ( + node.callee.type !== "MemberExpression" || + node.callee.computed || + node.callee.property.type !== "Identifier" || + node.callee.property.name !== "getChild" + ) + return; + + // The receiver must be one of the tracked logger names. + const object = node.callee.object; + if (object.type !== "Identifier" || !loggerNames.has(object.name)) + return; + + // Flag the call only when it is at module top level — i.e. there is no + // enclosing function or class body anywhere in the ancestor chain. + const ancestors = context.sourceCode.getAncestors(node); + const isTopLevel = !ancestors.some((a) => + FUNCTION_OR_CLASS_TYPES.has(a.type), + ); + + if (isTopLevel) { + context.report({ + messageId: "noTopLevelGetChild", + node, + }); + } + }, + }; + }, +}); + +export default rule; diff --git a/eslint/index.js b/eslint/index.js index ec050ad76..95b4d061f 100644 --- a/eslint/index.js +++ b/eslint/index.js @@ -2,5 +2,6 @@ module.exports = { rules: { "copyright-header": require("./CopyrightHeader").default, "no-observablescope-leak": require("./NoObservableScopeLeak").default, + "no-top-level-logger-get-child": require("./NoTopLevelLoggerGetChild").default, }, }; diff --git a/src/livekit/MatrixAudioRenderer.tsx b/src/livekit/MatrixAudioRenderer.tsx index 316294dac..e3970e9f3 100644 --- a/src/livekit/MatrixAudioRenderer.tsx +++ b/src/livekit/MatrixAudioRenderer.tsx @@ -14,7 +14,7 @@ import { AudioTrack, type AudioTrackProps, } from "@livekit/components-react"; -import { logger } from "matrix-js-sdk/lib/logger"; +import { logger as rootLogger } from "matrix-js-sdk/lib/logger"; import { useEarpieceAudioConfig } from "../MediaDevicesContext"; import { useReactiveState } from "../useReactiveState"; @@ -59,7 +59,7 @@ export function LivekitRoomAudioRenderer({ validIdentities, muted, }: MatrixAudioRendererProps): ReactNode { - const prefixedLogger = logger.getChild("[MatrixAudioRenderer]"); + const logger = rootLogger.getChild("[MatrixAudioRenderer]"); const tracks = useTracks( [ Track.Source.Microphone, @@ -80,7 +80,7 @@ export function LivekitRoomAudioRenderer({ if (!isValid) { // TODO make sure to also skip the warn logging for the local identity // Log that there is an invalid identity, that means that someone is publishing audio that is not expected to be in the call. - prefixedLogger.warn( + logger.warn( `Audio track ${ref.participant.identity} from ${url} has no matching matrix call member`, `current members: ${validIdentities.join()}`, `track will not get rendered`,