Enable restrict-template-expressions linting rule (#7181)

* [AI] Promote typescript/restrict-template-expressions to error and fix violations

Convert the oxlint rule from "warn" to "error" and fix all 42 violations
by wrapping non-string template expressions with String(). This ensures
type safety in template literals across the codebase.

https://claude.ai/code/session_01Uk8SwFbD6HuUuo3SSMwU9z

* Add release notes for PR #7181

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
This commit is contained in:
Matiss Janis Aboltins
2026-03-13 18:36:58 +00:00
committed by GitHub
parent 85e3166495
commit 541df52441
34 changed files with 61 additions and 42 deletions

View File

@@ -61,7 +61,7 @@ export class ConfigurationPage {
break;
default:
throw new Error(`Unrecognized import type: ${type}`);
throw new Error(`Unrecognized import type: ${String(type)}`);
}
const fileChooser = await fileChooserPromise;

View File

@@ -39,7 +39,7 @@ export class CustomReportPage {
.click();
break;
default:
throw new Error(`Unrecognized mode: ${mode}`);
throw new Error(`Unrecognized mode: ${String(mode)}`);
}
}
}

View File

@@ -779,7 +779,7 @@ export function useBudgetActions() {
});
return null;
default:
throw new Error(`Unknown budget action type: ${type}`);
throw new Error(`Unknown budget action type: ${String(type)}`);
}
},
onSuccess: notification => {

View File

@@ -95,7 +95,7 @@ export const HelpMenu = () => {
dispatch(pushModal({ modal: { name: 'goal-templates' } }));
break;
default:
throw new Error(`Unrecognized menu option: ${item}`);
throw new Error(`Unrecognized menu option: ${String(item)}`);
}
};

View File

@@ -45,7 +45,7 @@ export function IncomeMenu({
onClose();
break;
default:
throw new Error(`Unrecognized menu option: ${name}`);
throw new Error(`Unrecognized menu option: ${String(name)}`);
}
}}
items={[

View File

@@ -68,7 +68,9 @@ export const getInitialState = (template: Template | null): ReducerState => {
case 'error':
throw new Error('An error occurred while parsing the template');
default:
throw new Error(`Unknown template type: ${type satisfies undefined}`);
throw new Error(
`Unknown template type: ${String(type satisfies undefined)}`,
);
}
};
@@ -168,7 +170,9 @@ const changeType = (
};
default:
// Make sure we're not missing any cases
throw new Error(`Unknown display type: ${visualType satisfies never}`);
throw new Error(
`Unknown display type: ${String(visualType satisfies never)}`,
);
}
};
@@ -251,6 +255,6 @@ export const templateReducer = (
return mapTemplateTypesForUpdate(state, action.payload);
default:
// Make sure we're not missing any cases
throw new Error(`Unknown display type: ${type satisfies never}`);
throw new Error(`Unknown display type: ${String(type satisfies never)}`);
}
};

View File

@@ -34,7 +34,7 @@ export function BalanceMenu({
onCarryover?.(!carryover);
break;
default:
throw new Error(`Unrecognized menu option: ${name}`);
throw new Error(`Unrecognized menu option: ${String(name)}`);
}
}}
items={[

View File

@@ -506,7 +506,7 @@ function SelectedTransactionsFloatingActionBar({
: integerToCurrency(Number(value));
break;
case 'notes':
displayValue = `${mode} with ${value}`;
displayValue = `${mode} with ${String(value)}`;
break;
default:
displayValue = value;
@@ -514,15 +514,15 @@ function SelectedTransactionsFloatingActionBar({
}
showUndoNotification({
message: `Successfully updated ${name} of ${ids.length} transaction${ids.length > 1 ? 's' : ''} to [${displayValue}](#${displayValue}).`,
message: `Successfully updated ${name} of ${ids.length} transaction${ids.length > 1 ? 's' : ''} to [${String(displayValue)}](#${String(displayValue)}).`,
messageActions: {
[String(displayValue)]: () => {
switch (name) {
case 'account':
void navigate(`/accounts/${value}`);
void navigate(`/accounts/${String(value)}`);
break;
case 'category':
void navigate(`/categories/${value}`);
void navigate(`/categories/${String(value)}`);
break;
case 'payee':
void navigate(`/payees`);

View File

@@ -295,7 +295,7 @@ function AdditionalAccountMenu({
onToggleReconciled?.();
break;
default:
throw new Error(`Unrecognized menu option: ${name}`);
throw new Error(`Unrecognized menu option: ${String(name)}`);
}
}}
/>

View File

@@ -117,7 +117,7 @@ export function EnvelopeIncomeBalanceMenuModal({
onShowActivity?.();
break;
default:
throw new Error(`Unrecognized menu option: ${name}`);
throw new Error(`Unrecognized menu option: ${String(name)}`);
}
}}
items={[

View File

@@ -26,7 +26,7 @@ export function SelectField({
option,
hasHeaderRow
? option
: `Column ${parseInt(option) + 1} (${firstTransaction[option]})`,
: `Column ${parseInt(option) + 1} (${String(firstTransaction[option])})`,
] as const,
);

View File

@@ -128,7 +128,7 @@ export function SelectLinkedAccountsModal({
externalAccounts: toSort as SyncServerGoCardlessAccount[],
};
default:
throw new Error(`Unrecognized sync source: ${syncSource}`);
throw new Error(`Unrecognized sync source: ${String(syncSource)}`);
}
}, [externalAccounts, syncSource, requisitionId]);

View File

@@ -209,7 +209,7 @@ export const PayeeTableRow = memo(
onCreateRule(id);
break;
default:
throw new Error(`Unrecognized menu option: ${name}`);
throw new Error(`Unrecognized menu option: ${String(name)}`);
}
setMenuOpen(false);
}}

View File

@@ -671,7 +671,7 @@ export function Overview({ dashboard }: OverviewProps) {
break;
default:
throw new Error(
`Unrecognized menu option: ${item}`,
`Unrecognized menu option: ${String(item)}`,
);
}
}}

View File

@@ -99,7 +99,7 @@ export const RuleRow = memo(
onEditRule(rule);
break;
default:
throw new Error(`Unrecognized menu option: ${name}`);
throw new Error(`Unrecognized menu option: ${String(name)}`);
}
setMenuOpen(false);
}}

View File

@@ -137,7 +137,7 @@ export function Value<T>({
return '…';
default:
throw new Error(`Unknown field ${field}`);
throw new Error(`Unknown field ${String(field)}`);
}
}
}

View File

@@ -72,7 +72,7 @@ export function Schedules() {
await send('schedule/delete', { id });
break;
default:
throw new Error(`Unknown action: ${name}`);
throw new Error(`Unknown action: ${String(name)}`);
}
},
[],

View File

@@ -272,7 +272,9 @@ export function Account<FieldName extends SheetFields<'account'>>({
break;
}
default: {
throw new Error(`Unrecognized menu option: ${type}`);
throw new Error(
`Unrecognized menu option: ${String(type)}`,
);
}
}
setMenuOpen(false);

View File

@@ -114,7 +114,7 @@ export const TagRow = memo(
deleteTag({ id: tag.id });
break;
default:
throw new Error(`Unrecognized menu option: ${name}`);
throw new Error(`Unrecognized menu option: ${String(name)}`);
}
setMenuOpen(false);
}}

View File

@@ -400,7 +400,7 @@ expect.extend({
} else {
return {
message: () =>
`Expected ${validPayeeListWithFavorite} to have favorite stars`,
`Expected ${String(validPayeeListWithFavorite)} to have favorite stars`,
pass: true,
};
}

View File

@@ -120,10 +120,10 @@ export function useTransactionBatchActions() {
if (name === 'notes') {
if (mode === 'prepend') {
valueToSet =
trans.notes === null ? value : `${value}${trans.notes}`;
trans.notes === null ? value : `${String(value)}${trans.notes}`;
} else if (mode === 'append') {
valueToSet =
trans.notes === null ? value : `${trans.notes}${value}`;
trans.notes === null ? value : `${trans.notes}${String(value)}`;
} else if (mode === 'replace') {
valueToSet = value;
} else if (

View File

@@ -323,7 +323,10 @@ async function startSyncServer() {
return await Promise.race([syncServerPromise, syncServerTimeout]); // Either the server has started or the timeout is reached
} catch (error) {
logMessage('error', `Sync-Server: Error starting sync server: ${error}`);
logMessage(
'error',
`Sync-Server: Error starting sync server: ${String(error)}`,
);
}
}
@@ -661,7 +664,7 @@ ipcMain.handle(
} catch (error) {
logMessage(
'error',
`There was an error moving your directory: ${error}`,
`There was an error moving your directory: ${String(error)}`,
);
throw error;
}
@@ -687,7 +690,7 @@ ipcMain.handle(
// This call needs to succeed to allow the user to continue using the app with the files in the new location.
logMessage(
'error',
`There was an error removing the old directory: ${error}`,
`There was an error removing the old directory: ${String(error)}`,
);
}
},

View File

@@ -35,7 +35,7 @@ const lazyLoadBackend = async (isDev: boolean) => {
} catch (error) {
console.error('Failed to init the server bundle after all retries:', error);
throw new Error(
`Failed to init the server bundle after all retries: ${error}`,
`Failed to init the server bundle after all retries: ${String(error)}`,
);
}
};

View File

@@ -76,7 +76,7 @@ export function patchFetchForSqlJS(baseURL: string) {
},
});
}
return Promise.reject(new Error(`fetch not mocked for ${url}`));
return Promise.reject(new Error(`fetch not mocked for ${String(url)}`));
},
);
}

View File

@@ -944,7 +944,7 @@ handlers['api/schedule-update'] = withMutation(async function ({
break;
default:
throw APIError(
`Invalid amount operator: ${value}. Expected: is, isapprox, or isbetween`,
`Invalid amount operator: ${String(value)}. Expected: is, isapprox, or isbetween`,
);
}
sched._conditions[amountIndex].op = convertedOp;

View File

@@ -715,7 +715,11 @@ const compileOp = saveStack('op', (state, fieldRef, opData) => {
// Dedupe the ids
const ids = [...new Set(right)];
return `${left} IN (` + ids.map(id => `'${id}'`).join(',') + ')';
return (
`${String(left)} IN (` +
ids.map(id => `'${String(id)}'`).join(',') +
')'
);
}
case '$like': {
const [left, right] = valArray(state, [lhs, rhs], ['string', 'string']);

View File

@@ -611,7 +611,7 @@ export class CategoryTemplateContext {
monthUtils.addMonths(date, numPeriods * 12);
break;
default:
throw new Error(`Unrecognized periodic period: ${period}`);
throw new Error(`Unrecognized periodic period: ${String(period)}`);
}
//shift the starting date until its in our month or in the future

View File

@@ -89,7 +89,7 @@ const exportModel = {
if (!isWidgetType(widget.type)) {
throw new ValidationError(
`Invalid widget.${idx}.type value ${widget.type}.`,
`Invalid widget.${idx}.type value ${String(widget.type)}.`,
);
}

View File

@@ -24,7 +24,7 @@ export const mappingsFromString = (str: string): Mappings => {
);
} catch (e) {
const message = e instanceof Error ? e.message : e;
throw new Error(`Failed to parse mapping: ${message}`);
throw new Error(`Failed to parse mapping: ${String(message)}`);
}
};

View File

@@ -79,7 +79,7 @@ export function getDownloadError({
default:
const info =
meta && typeof meta === 'object' && 'fileId' in meta && meta.fileId
? `, fileId: ${meta.fileId}`
? `, fileId: ${String(meta.fileId)}`
: '';
return t(
'Something went wrong trying to download that file, sorry! Visit https://actualbudget.org/contact/ for support. reason: {{reason}}{{info}}',

View File

@@ -91,12 +91,12 @@ if (values.config) {
if (!configExists) {
console.log(
`Please specify a valid config path. The path ${values.config} does not exist.`,
`Please specify a valid config path. The path ${String(values.config)} does not exist.`,
);
process.exit();
} else {
console.log(`Loading config from ${values.config}`);
console.log(`Loading config from ${String(values.config)}`);
const configJson = JSON.parse(readFileSync(values.config, 'utf-8'));
process.env.ACTUAL_CONFIG_PATH = values.config;
setupDataDir(configJson.dataDir);

View File

@@ -55,7 +55,7 @@ const requestLoggerMiddleware = expressWinston.logger({
const { timestamp, level, meta } = args;
const { res, req } = meta as { res: Response; req: Request };
return `${timestamp} ${level}: ${req.method} ${res.statusCode} ${req.url}`;
return `${String(timestamp)} ${String(level)}: ${req.method} ${res.statusCode} ${req.url}`;
}),
),
});