From 5e3aeb37e107fcbd0ee9fe4cd89357f054bb335c Mon Sep 17 00:00:00 2001 From: dyoshikawa-coding Date: Sat, 5 Jul 2025 07:34:47 +0000 Subject: [PATCH] refactor: reduce unsafe 'as' type assertions throughout codebase MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Improved type safety by: - Adding proper type guards before type assertions in JSON parsers - Replacing unsafe type coercion with explicit type checks - Using explicit conditional checks instead of unsafe key lookups - Adding validation before casting unknown types 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- src/cli/commands/status.ts | 8 +++++--- src/core/mcp-parser.ts | 12 +++++++++--- src/core/parser.ts | 1 + src/generators/mcp/claudecode.ts | 4 +++- src/generators/mcp/cursor.ts | 2 +- src/generators/mcp/roo.ts | 2 +- src/parsers/claudecode.ts | 10 +++++++--- src/parsers/cursor.ts | 16 +++++++++++++--- src/parsers/geminicli.ts | 7 ++++++- 9 files changed, 46 insertions(+), 16 deletions(-) diff --git a/src/cli/commands/status.ts b/src/cli/commands/status.ts index b13ff8c0d..f878f0795 100644 --- a/src/cli/commands/status.ts +++ b/src/cli/commands/status.ts @@ -37,9 +37,11 @@ export async function statusCommand(): Promise { rule.frontmatter.targets[0] === "*" ? config.defaultTargets : rule.frontmatter.targets; for (const target of targets) { - if (target in targetCounts) { - targetCounts[target as keyof typeof targetCounts]++; - } + if (target === "copilot") targetCounts.copilot++; + else if (target === "cursor") targetCounts.cursor++; + else if (target === "cline") targetCounts.cline++; + else if (target === "claudecode") targetCounts.claudecode++; + else if (target === "roo") targetCounts.roo++; } } diff --git a/src/core/mcp-parser.ts b/src/core/mcp-parser.ts index 9471aea86..0acdd57a9 100644 --- a/src/core/mcp-parser.ts +++ b/src/core/mcp-parser.ts @@ -1,6 +1,6 @@ import * as fs from "node:fs"; import * as path from "node:path"; -import type { RulesyncMcpConfig } from "../types/mcp.js"; +import type { RulesyncMcpConfig, RulesyncMcpServer } from "../types/mcp.js"; export function parseMcpConfig(projectRoot: string): RulesyncMcpConfig | null { const mcpPath = path.join(projectRoot, ".rulesync", ".mcp.json"); @@ -11,7 +11,13 @@ export function parseMcpConfig(projectRoot: string): RulesyncMcpConfig | null { try { const content = fs.readFileSync(mcpPath, "utf-8"); - const rawConfig = JSON.parse(content) as Record; + const parsed = JSON.parse(content); + + if (!parsed || typeof parsed !== "object") { + throw new Error("Invalid mcp.json: must be an object"); + } + + const rawConfig = parsed as Record; // Handle legacy 'servers' field and migrate to 'mcpServers' if (rawConfig.servers && !rawConfig.mcpServers) { @@ -28,7 +34,7 @@ export function parseMcpConfig(projectRoot: string): RulesyncMcpConfig | null { delete rawConfig.tools; } - return { mcpServers: rawConfig.mcpServers } as RulesyncMcpConfig; + return { mcpServers: rawConfig.mcpServers as Record }; } catch (error) { throw new Error( `Failed to parse mcp.json: ${error instanceof Error ? error.message : String(error)}`, diff --git a/src/core/parser.ts b/src/core/parser.ts index 38501bfcc..dc2d4b215 100644 --- a/src/core/parser.ts +++ b/src/core/parser.ts @@ -47,6 +47,7 @@ export async function parseRuleFile(filepath: string): Promise { // Validate frontmatter validateFrontmatter(parsed.data, filepath); + // After validation, we can safely cast since validateFrontmatter ensures the shape const frontmatter = parsed.data as RuleFrontmatter; const filename = basename(filepath, ".md"); diff --git a/src/generators/mcp/claudecode.ts b/src/generators/mcp/claudecode.ts index 813cfe538..ca117a744 100644 --- a/src/generators/mcp/claudecode.ts +++ b/src/generators/mcp/claudecode.ts @@ -77,7 +77,9 @@ export function generateClaudeMcpConfiguration( // Only add transport if it's supported by Claude if (transport && transport !== "stdio") { - claudeServer.transport = transport as "sse" | "http"; + if (transport === "sse" || transport === "http") { + claudeServer.transport = transport; + } } settings.mcpServers![serverName] = claudeServer; diff --git a/src/generators/mcp/cursor.ts b/src/generators/mcp/cursor.ts index 4302c99df..e323bfbbe 100644 --- a/src/generators/mcp/cursor.ts +++ b/src/generators/mcp/cursor.ts @@ -69,7 +69,7 @@ export function generateCursorMcpConfiguration( continue; } - // Cast to RulesyncMcpServer for type safety + // Cast to RulesyncMcpServer after type check const serverObj = server as RulesyncMcpServer; // Check if this server should be included for cursor diff --git a/src/generators/mcp/roo.ts b/src/generators/mcp/roo.ts index f3832872a..9764c13d0 100644 --- a/src/generators/mcp/roo.ts +++ b/src/generators/mcp/roo.ts @@ -86,7 +86,7 @@ export function generateRooMcpConfiguration( continue; } - // Cast to RulesyncMcpServer for type safety + // Cast to RulesyncMcpServer after type check const serverObj = server as RulesyncMcpServer; // Check if this server should be included for roo diff --git a/src/parsers/claudecode.ts b/src/parsers/claudecode.ts index 1af853d32..860b4fe31 100644 --- a/src/parsers/claudecode.ts +++ b/src/parsers/claudecode.ts @@ -160,9 +160,13 @@ async function parseClaudeSettings(settingsPath: string): Promise; - if (permissions && "deny" in permissions && Array.isArray(permissions.deny)) { - const readPatterns = permissions.deny + const permissions = settings.permissions; + if (typeof permissions !== "object" || permissions === null) { + return { errors }; + } + const permsObj = permissions as Record; + if (permsObj && "deny" in permsObj && Array.isArray(permsObj.deny)) { + const readPatterns = permsObj.deny .filter( (rule): rule is string => typeof rule === "string" && rule.startsWith("Read(") && rule.endsWith(")"), diff --git a/src/parsers/cursor.ts b/src/parsers/cursor.ts index 3f4aac55d..0a32b70ee 100644 --- a/src/parsers/cursor.ts +++ b/src/parsers/cursor.ts @@ -33,11 +33,13 @@ const customMatterOptions = { // But exclude array literals (starting with [ or already quoted strings) .replace(/^(\s*globs:\s*)([^\s"'[\n][^"'[\n]*?)(\s*)$/gm, '$1"$2"$3'); - return load(preprocessed, { schema: DEFAULT_SCHEMA }) as object; + const result = load(preprocessed, { schema: DEFAULT_SCHEMA }); + return result as object; } catch (error) { // If that fails, try with FAILSAFE_SCHEMA as a fallback try { - return load(str, { schema: FAILSAFE_SCHEMA }) as object; + const result = load(str, { schema: FAILSAFE_SCHEMA }); + return result as object; } catch { // If all else fails, throw the original error throw error; @@ -56,6 +58,9 @@ function convertCursorMdcFrontmatter( _filename: string, ): RuleFrontmatter { // Type guard to ensure we have an object + if (!cursorFrontmatter || typeof cursorFrontmatter !== "object") { + throw new Error("Invalid frontmatter: expected object"); + } const frontmatter = cursorFrontmatter as Record; // Normalize values according to term definitions @@ -274,7 +279,12 @@ export async function parseCursorConfiguration( try { const content = await readFileContent(cursorMcpPath); const mcp = JSON.parse(content); - if (mcp.mcpServers && Object.keys(mcp.mcpServers).length > 0) { + if ( + mcp && + typeof mcp === "object" && + mcp.mcpServers && + Object.keys(mcp.mcpServers).length > 0 + ) { mcpServers = mcp.mcpServers as Record; } } catch (error) { diff --git a/src/parsers/geminicli.ts b/src/parsers/geminicli.ts index 487fa944a..f9858d857 100644 --- a/src/parsers/geminicli.ts +++ b/src/parsers/geminicli.ts @@ -169,7 +169,12 @@ async function parseGeminiSettings(settingsPath: string): Promise 0) { + if ( + settings && + typeof settings === "object" && + settings.mcpServers && + Object.keys(settings.mcpServers).length > 0 + ) { mcpServers = settings.mcpServers as Record; } } catch (error) {