#!/usr/bin/env node /* Copyright 2026 Element Creations Ltd. SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial Please see LICENSE files in the repository root for full details. */ // Finds settings declared in apps/web/src/settings/Settings.tsx which are never // referenced anywhere else in apps/ outside of settings/ directories. This is a // rough heuristic (plain string search over the codebase) rather than a full // type-aware usage analysis, but setting names are unique enough in practice. import * as fs from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; import { execFileSync } from "node:child_process"; import ts from "typescript"; const ROOT = fileURLToPath(import.meta.resolve("..")); const SETTINGS_DIR = path.join(ROOT, "apps/web/src/settings"); const SETTINGS_FILE = path.join(SETTINGS_DIR, "Settings.tsx"); const EXCLUDE_GLOBS = [ // Only the settings *definitions* directory should be excluded from the usage search - // there are plenty of other directories literally named "settings" (e.g. // apps/web/src/components/views/settings/) which hold real usages and must stay included. "apps/web/src/settings/**", "*/*/test/**", "*-test.*", "*.test.*", ].map((pattern) => path.relative(ROOT, pattern)); // See https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#setting-an-error-message const SETTINGS_FILE_RELATIVE = path.relative(ROOT, SETTINGS_FILE); // Settings that are only ever referenced from inside another setting's `controller: ...` // expression in Settings.tsx (both live in the excluded settings/ directory) are treated as // used. This is detected automatically by scanning controller text for other settings' // search terms, but keep this list as a manual escape hatch for cases the heuristic can't // see (e.g. usage mediated through a helper function rather than a literal reference). const KNOWN_USED_OVERRIDES = new Set([ "test_setting", // only used in tests ]); interface DeclaredSetting { // The literal setting name (as passed to SettingsStore), used for reporting. name: string; // Text(s) that count as "usage" when found via git grep, e.g. both the enum // member reference (`UIFeature.Registration`) and its resolved value. searchTerms: string[]; line: number; // Source text of this setting's own `controller: ...` property, if any. Settings are // sometimes only ever read by another setting's controller (e.g. a UIFeatureController // gating a different setting) - see KNOWN_USED_VIA_CONTROLLER below for how this is used. controllerText?: string; } function parseFile(filePath: string): ts.SourceFile { const content = fs.readFileSync(filePath, "utf-8"); return ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX); } /** Maps `EnumName.MemberName` -> its string literal value, for every string enum under settings/. */ function buildEnumLookup(): Map { const lookup = new Map(); const files = fs.readdirSync(SETTINGS_DIR).filter((f) => f.endsWith(".ts") || f.endsWith(".tsx")); for (const file of files) { const sourceFile = parseFile(path.join(SETTINGS_DIR, file)); const visit = (node: ts.Node): void => { if (ts.isEnumDeclaration(node)) { for (const member of node.members) { if (member.initializer && ts.isStringLiteral(member.initializer)) { const memberName = member.name.getText(sourceFile); lookup.set(`${node.name.text}.${memberName}`, member.initializer.text); } } } ts.forEachChild(node, visit); }; visit(sourceFile); } return lookup; } /** Extracts every top-level key of `export const SETTINGS = { ... }` via the TS AST. */ function extractSettingNames(enumLookup: Map): DeclaredSetting[] { const sourceFile = parseFile(SETTINGS_FILE); const settings: DeclaredSetting[] = []; let settingsObject: ts.ObjectLiteralExpression | undefined; ts.forEachChild(sourceFile, (node) => { if ( ts.isVariableStatement(node) && node.declarationList.declarations.some((d) => ts.isIdentifier(d.name) && d.name.text === "SETTINGS") ) { const decl = node.declarationList.declarations.find( (d) => ts.isIdentifier(d.name) && d.name.text === "SETTINGS", )!; if (decl.initializer && ts.isObjectLiteralExpression(decl.initializer)) { settingsObject = decl.initializer; } } }); if (!settingsObject) { throw new Error("Could not find `export const SETTINGS` object literal in Settings.tsx"); } for (const prop of settingsObject.properties) { if (!ts.isPropertyAssignment(prop)) continue; const line = sourceFile.getLineAndCharacterOfPosition(prop.getStart(sourceFile)).line + 1; const propName = prop.name; const controllerText = findControllerText(prop.initializer, sourceFile); if (ts.isIdentifier(propName) || ts.isStringLiteral(propName)) { const name = propName.text; settings.push({ name, searchTerms: [name], line, controllerText }); } else if (ts.isComputedPropertyName(propName)) { const enumRef = propName.expression.getText(sourceFile); const value = enumLookup.get(enumRef); if (!value) { console.warn(`Warning: could not resolve computed setting key [${enumRef}] at Settings.tsx:${line}`); continue; } // Code typically references these via the enum member (e.g. `UIFeature.Registration`) // rather than the resolved string, so both count as usage. settings.push({ name: value, searchTerms: [enumRef, value], line, controllerText }); } } return settings; } /** Returns the source text of a setting definition's `controller: ...` property, if it has one. */ function findControllerText(settingValue: ts.Expression, sourceFile: ts.SourceFile): string | undefined { if (!ts.isObjectLiteralExpression(settingValue)) return undefined; for (const member of settingValue.properties) { if (ts.isPropertyAssignment(member) && ts.isIdentifier(member.name) && member.name.text === "controller") { return member.initializer.getText(sourceFile); } } return undefined; } /** * Checks whether any of the given search terms appears anywhere under apps/ outside the * settings definitions directory. Relies on the `git` binary being available on PATH. */ function isUsedOutsideSettings(searchTerms: string[]): boolean { const patternArgs = searchTerms.flatMap((term) => ["-e", term]); const excludeArgs = EXCLUDE_GLOBS.map((term) => `:(exclude)${term}`); try { execFileSync("git", ["grep", "-F", "-q", ...patternArgs, "--", "apps", ...excludeArgs], { cwd: ROOT }); return true; } catch (e) { if (typeof (e as { status?: number }).status === "number") return false; throw e; } } /** Checks whether another setting's `controller` expression references one of this setting's search terms. */ function isReferencedByOtherController(setting: DeclaredSetting, allSettings: DeclaredSetting[]): boolean { return allSettings.some( (other) => other.name !== setting.name && other.controllerText !== undefined && setting.searchTerms.some((term) => other.controllerText!.includes(term)), ); } function printAnnotation(line: number, message: string): void { const escape = (s: string): string => s.replaceAll("%", "%25").replaceAll("\r", "%0D").replaceAll("\n", "%0A"); console.log(`::error file=${SETTINGS_FILE_RELATIVE},line=${line},title=Unused setting::${escape(message)}`); } /** De-duplicates settings by name, keeping the first occurrence. */ function dedupeSettingsByName(settings: DeclaredSetting[]): Map { const firstByName = new Map(); for (const setting of settings) { if (!firstByName.has(setting.name)) firstByName.set(setting.name, setting); } return firstByName; } function findUnusedSettings(candidates: Iterable, allSettings: DeclaredSetting[]): DeclaredSetting[] { const unused: DeclaredSetting[] = []; for (const setting of candidates) { if (KNOWN_USED_OVERRIDES.has(setting.name)) continue; if (isUsedOutsideSettings(setting.searchTerms)) continue; if (isReferencedByOtherController(setting, allSettings)) continue; unused.push(setting); } return unused; } function reportUnused(unused: DeclaredSetting[]): void { unused.sort((a, b) => a.line - b.line); console.error(`⛔ Found ${unused.length} setting(s) declared with no usage:\n`); for (const { name, line } of unused) { console.error(` Settings.tsx:${line}: "${name}"`); if (process.env.GITHUB_ACTIONS === "true") { printAnnotation(line, `Setting "${name}" is declared but never used outside ${EXCLUDE_GLOBS}`); } } } function main(): void { const enumLookup = buildEnumLookup(); const settings = extractSettingNames(enumLookup); const firstByName = dedupeSettingsByName(settings); const unused = findUnusedSettings(firstByName.values(), settings); if (unused.length > 0) { reportUnused(unused); process.exit(1); } console.log(`✅ All ${firstByName.size} settings appear to be used.`); } main();