Disallow unsafe type assertions (#18688)

This commit is contained in:
Christian Gunderman
2026-02-10 00:10:15 +00:00
committed by GitHub
parent bce1caefd0
commit fd65416a2f
188 changed files with 592 additions and 47 deletions
+7
View File
@@ -281,6 +281,7 @@ export async function parseArguments(
.check((argv) => {
// The 'query' positional can be a string (for one arg) or string[] (for multiple).
// This guard safely checks if any positional argument was provided.
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
const query = argv['query'] as string | string[] | undefined;
const hasPositionalQuery = Array.isArray(query)
? query.length > 0
@@ -298,6 +299,7 @@ export async function parseArguments(
if (
argv['outputFormat'] &&
!['text', 'json', 'stream-json'].includes(
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
argv['outputFormat'] as string,
)
) {
@@ -346,6 +348,7 @@ export async function parseArguments(
}
// Normalize query args: handle both quoted "@path file" and unquoted @path file
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
const queryArg = (result as { query?: string | string[] | undefined }).query;
const q: string | undefined = Array.isArray(queryArg)
? queryArg.join(' ')
@@ -369,6 +372,7 @@ export async function parseArguments(
// The import format is now only controlled by settings.memoryImportFormat
// We no longer accept it as a CLI argument
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
return result as unknown as CliArgs;
}
@@ -477,6 +481,7 @@ export async function loadCliConfig(
requestSetting: promptForSetting,
workspaceDir: cwd,
enabledExtensionOverrides: argv.extensions,
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
eventEmitter: coreEvents as EventEmitter<ExtensionEvents>,
clientVersion: await getVersion(),
});
@@ -580,6 +585,7 @@ export async function loadCliConfig(
let telemetrySettings;
try {
telemetrySettings = await resolveTelemetrySettings({
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
env: process.env as unknown as Record<string, string | undefined>,
settings: settings.telemetry,
});
@@ -809,6 +815,7 @@ export async function loadCliConfig(
eventEmitter: coreEvents,
useWriteTodos: argv.useWriteTodos ?? settings.useWriteTodos,
output: {
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
format: (argv.outputFormat ?? settings.output?.format) as OutputFormat,
},
fakeResponses: argv.fakeResponses,
@@ -85,6 +85,7 @@ describe('ExtensionManager theme loading', () => {
await extensionManager.loadExtensions();
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
const mockConfig = {
getEnableExtensionReloading: () => false,
getMcpClientManager: () => ({
@@ -170,6 +171,7 @@ describe('ExtensionManager theme loading', () => {
await extensionManager.loadExtensions();
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
const mockConfig = {
getWorkingDir: () => tempHomeDir,
shouldLoadMemoryFromIncludeDirectories: () => false,
@@ -730,6 +730,7 @@ Would you like to attempt to install via "git clone" instead?`,
if (Object.keys(hookEnv).length > 0) {
for (const eventName of Object.keys(hooks)) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
const eventHooks = hooks[eventName as HookEventName];
if (eventHooks) {
for (const definition of eventHooks) {
@@ -826,13 +827,16 @@ Would you like to attempt to install via "git clone" instead?`,
}
try {
const configContent = await fs.promises.readFile(configFilePath, 'utf-8');
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
const rawConfig = JSON.parse(configContent) as ExtensionConfig;
if (!rawConfig.name || !rawConfig.version) {
throw new Error(
`Invalid configuration in ${configFilePath}: missing ${!rawConfig.name ? '"name"' : '"version"'}`,
);
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
const config = recursivelyHydrateStrings(
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
rawConfig as unknown as JsonObject,
{
extensionPath: extensionDir,
@@ -878,6 +882,7 @@ Would you like to attempt to install via "git clone" instead?`,
// Hydrate variables in the hooks configuration
const hydratedHooks = recursivelyHydrateStrings(
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
rawHooks.hooks as unknown as JsonObject,
{
...context,
@@ -888,6 +893,7 @@ Would you like to attempt to install via "git clone" instead?`,
return hydratedHooks;
} catch (e) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
if ((e as NodeJS.ErrnoException).code === 'ENOENT') {
return undefined; // File not found is not an error here.
}
+1
View File
@@ -47,6 +47,7 @@ export function loadInstallMetadata(
const metadataFilePath = path.join(extensionDir, INSTALL_METADATA_FILENAME);
try {
const configContent = fs.readFileSync(metadataFilePath, 'utf-8');
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
const metadata = JSON.parse(configContent) as ExtensionInstallMetadata;
return metadata;
} catch (_e) {
@@ -105,6 +105,7 @@ export class ExtensionRegistryClient {
throw new Error(`Failed to fetch extensions: ${response.statusText}`);
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
return (await response.json()) as RegistryExtension[];
} catch (error) {
// Clear the promise on failure so that subsequent calls can try again
@@ -45,6 +45,7 @@ export async function fetchJson<T>(
res.on('data', (chunk) => chunks.push(chunk));
res.on('end', () => {
const data = Buffer.concat(chunks).toString();
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
resolve(JSON.parse(data) as T);
});
})
@@ -52,9 +52,11 @@ export function recursivelyHydrateStrings<T>(
values: VariableContext,
): T {
if (typeof obj === 'string') {
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
return hydrateString(obj, values) as unknown as T;
}
if (Array.isArray(obj)) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
return obj.map((item) =>
recursivelyHydrateStrings(item, values),
) as unknown as T;
@@ -64,11 +66,13 @@ export function recursivelyHydrateStrings<T>(
for (const key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
newObj[key] = recursivelyHydrateStrings(
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
(obj as Record<string, unknown>)[key],
values,
);
}
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
return newObj as T;
}
return obj;
@@ -358,6 +358,7 @@ export class McpServerEnablementManager {
private async readConfig(): Promise<McpServerEnablementConfig> {
try {
const content = await fs.readFile(this.configFilePath, 'utf-8');
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
return JSON.parse(content) as McpServerEnablementConfig;
} catch (error) {
if (
@@ -23,6 +23,7 @@ function buildZodSchemaFromJsonSchema(def: any): z.ZodTypeAny {
}
if (def.type === 'string') {
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
if (def.enum) return z.enum(def.enum as [string, ...string[]]);
return z.string();
}
@@ -40,7 +41,7 @@ function buildZodSchemaFromJsonSchema(def: any): z.ZodTypeAny {
let schema;
if (def.properties) {
const shape: Record<string, z.ZodTypeAny> = {};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-type-assertion
for (const [key, propDef] of Object.entries(def.properties) as any) {
let propSchema = buildZodSchemaFromJsonSchema(propDef);
if (
@@ -86,9 +87,11 @@ function buildEnumSchema(
}
const values = options.map((opt) => opt.value);
if (values.every((v) => typeof v === 'string')) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
return z.enum(values as [string, ...string[]]);
} else if (values.every((v) => typeof v === 'number')) {
return z.union(
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
values.map((v) => z.literal(v)) as [
z.ZodLiteral<number>,
z.ZodLiteral<number>,
@@ -97,6 +100,7 @@ function buildEnumSchema(
);
} else {
return z.union(
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
values.map((v) => z.literal(v)) as [
z.ZodLiteral<unknown>,
z.ZodLiteral<unknown>,
+15
View File
@@ -213,6 +213,7 @@ function setNestedProperty(
}
const next = current[key];
if (typeof next === 'object' && next !== null) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
current = next as Record<string, unknown>;
} else {
// This path is invalid, so we stop.
@@ -254,6 +255,7 @@ export function mergeSettings(
// 3. User Settings
// 4. Workspace Settings
// 5. System Settings (as overrides)
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
return customDeepMerge(
getMergeStrategyForPath,
schemaDefaults,
@@ -274,6 +276,7 @@ export function mergeSettings(
export function createTestMergedSettings(
overrides: Partial<Settings> = {},
): MergedSettings {
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
return customDeepMerge(
getMergeStrategyForPath,
getDefaultsFromSchema(),
@@ -355,6 +358,7 @@ export class LoadedSettings {
// The final admin settings are the defaults overridden by remote settings.
// Any admin settings from files are ignored.
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
merged.admin = customDeepMerge(
(path: string[]) => getMergeStrategyForPath(['admin', ...path]),
adminDefaults,
@@ -617,6 +621,7 @@ export function loadSettings(
return { settings: {} };
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
const settingsObject = rawSettings as Record<string, unknown>;
// Validate settings structure with Zod
@@ -850,6 +855,7 @@ export function migrateDeprecatedSettings(
const uiSettings = settings.ui as Record<string, unknown> | undefined;
if (uiSettings) {
const newUi = { ...uiSettings };
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
const accessibilitySettings = newUi['accessibility'] as
| Record<string, unknown>
| undefined;
@@ -880,6 +886,7 @@ export function migrateDeprecatedSettings(
| undefined;
if (contextSettings) {
const newContext = { ...contextSettings };
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
const fileFilteringSettings = newContext['fileFiltering'] as
| Record<string, unknown>
| undefined;
@@ -1000,6 +1007,7 @@ function migrateExperimentalSettings(
...(settings.agents as Record<string, unknown> | undefined),
};
const agentsOverrides = {
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
...((agentsSettings['overrides'] as Record<string, unknown>) || {}),
};
let modified = false;
@@ -1011,6 +1019,7 @@ function migrateExperimentalSettings(
const old = experimentalSettings[oldKey];
if (old) {
foundDeprecated?.push(`experimental.${oldKey}`);
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
migrateFn(old as Record<string, unknown>);
modified = true;
}
@@ -1019,6 +1028,7 @@ function migrateExperimentalSettings(
// Migrate codebaseInvestigatorSettings -> agents.overrides.codebase_investigator
migrateExperimental('codebaseInvestigatorSettings', (old) => {
const override = {
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
...(agentsOverrides['codebase_investigator'] as
| Record<string, unknown>
| undefined),
@@ -1027,6 +1037,7 @@ function migrateExperimentalSettings(
if (old['enabled'] !== undefined) override['enabled'] = old['enabled'];
const runConfig = {
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
...(override['runConfig'] as Record<string, unknown> | undefined),
};
if (old['maxNumTurns'] !== undefined)
@@ -1037,16 +1048,19 @@ function migrateExperimentalSettings(
if (old['model'] !== undefined || old['thinkingBudget'] !== undefined) {
const modelConfig = {
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
...(override['modelConfig'] as Record<string, unknown> | undefined),
};
if (old['model'] !== undefined) modelConfig['model'] = old['model'];
if (old['thinkingBudget'] !== undefined) {
const generateContentConfig = {
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
...(modelConfig['generateContentConfig'] as
| Record<string, unknown>
| undefined),
};
const thinkingConfig = {
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
...(generateContentConfig['thinkingConfig'] as
| Record<string, unknown>
| undefined),
@@ -1064,6 +1078,7 @@ function migrateExperimentalSettings(
// Migrate cliHelpAgentSettings -> agents.overrides.cli_help
migrateExperimental('cliHelpAgentSettings', (old) => {
const override = {
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
...(agentsOverrides['cli_help'] as Record<string, unknown> | undefined),
};
if (old['enabled'] !== undefined) override['enabled'] = old['enabled'];
@@ -47,6 +47,7 @@ export function isTrustLevel(
): value is TrustLevel {
return (
typeof value === 'string' &&
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
Object.values(TrustLevel).includes(value as TrustLevel)
);
}
@@ -197,6 +198,7 @@ export class LoadedTrustedFolders {
const content = await fsPromises.readFile(this.user.path, 'utf-8');
let config: Record<string, TrustLevel>;
try {
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
config = parseTrustedFoldersJson(content) as Record<string, TrustLevel>;
} catch (error) {
coreEvents.emitFeedback(
@@ -251,6 +253,7 @@ export function loadTrustedFolders(): LoadedTrustedFolders {
try {
if (fs.existsSync(userPath)) {
const content = fs.readFileSync(userPath, 'utf-8');
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
const parsed = parseTrustedFoldersJson(content) as Record<string, string>;
if (