From debd3f218e797372969e366aa9a19ae443330ee1 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 21 Jan 2026 08:46:53 +0000 Subject: [PATCH] Fix Swift syntax error in scaffolded projects Replace Kotlin 'val' keyword with Swift 'let' during template processing. This prevents compilation errors when scaffolding iOS/macOS projects. - Added fixSwiftSyntax() to convert 'val' to 'let' in Swift files - Applied fix to both iOS and macOS scaffold processing - Added tests to verify the fix works correctly - Updated CHANGELOG.md Fixes XCODEBUILD-MCP-132X --- CHANGELOG.md | 1 + .../__tests__/scaffold_ios_project.test.ts | 110 ++++++++++++++++++ .../scaffold_ios_project.ts | 19 +++ .../scaffold_macos_project.ts | 19 +++ 4 files changed, 149 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f6d309de..50f5cf38 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ - Update UI automation guard guidance to point at `debug_continue` when paused. - Fix tool loading bugs in static tool registration. - Fix xcodemake command argument corruption when project directory path appears as substring in non-path arguments. +- Fixed Swift syntax error in scaffolded projects by replacing Kotlin 'val' keyword with Swift 'let' (Fixes XCODEBUILD-MCP-132X). ## [1.16.0] - 2025-12-30 - Remove dynamic tool discovery (`discover_tools`) and `XCODEBUILDMCP_DYNAMIC_TOOLS`. Use `XCODEBUILDMCP_ENABLED_WORKFLOWS` to limit startup tool registration. diff --git a/src/mcp/tools/project-scaffolding/__tests__/scaffold_ios_project.test.ts b/src/mcp/tools/project-scaffolding/__tests__/scaffold_ios_project.test.ts index 28866c9a..4a1c1405 100644 --- a/src/mcp/tools/project-scaffolding/__tests__/scaffold_ios_project.test.ts +++ b/src/mcp/tools/project-scaffolding/__tests__/scaffold_ios_project.test.ts @@ -666,4 +666,114 @@ describe('scaffold_ios_project plugin', () => { process.env.XCODEBUILDMCP_IOS_TEMPLATE_PATH = '/mock/template/path'; }); }); + + describe('Swift Syntax Fixes', () => { + it('should fix Kotlin val keyword in Swift test files', async () => { + // Track written files + let writtenFiles: Record = {}; + const trackingFileSystemExecutor = createMockFileSystemExecutor({ + existsSync: (path) => { + return ( + path.includes('xcodebuild-mcp-template') || + path.includes('XcodeBuildMCP-iOS-Template') || + path.includes('/template') || + path.endsWith('template') || + path.includes('extracted') || + path.includes('/mock/template/path') + ); + }, + readFile: async (path) => { + // Simulate a Swift test file with Kotlin 'val' keyword + if (path.includes('.swift')) { + return 'val equal = XCTAssertEqual(actual, expected, message, file: file, line: line)'; + } + return 'template content with MyProject placeholder'; + }, + readdir: async () => [ + { name: 'MyProjectTests.swift', isDirectory: () => false, isFile: () => true } as any, + ], + mkdir: async () => {}, + rm: async () => {}, + cp: async () => {}, + writeFile: async (path, content) => { + writtenFiles[path] = content as string; + }, + stat: async () => ({ isDirectory: () => true, mtimeMs: 0 }), + }); + + await scaffold_ios_projectLogic( + { + projectName: 'TestApp', + customizeNames: true, + outputPath: '/tmp/test-projects', + }, + mockCommandExecutor, + trackingFileSystemExecutor, + ); + + // Verify that the written Swift file has 'let' instead of 'val' + const swiftFiles = Object.entries(writtenFiles).filter(([path]) => path.endsWith('.swift')); + expect(swiftFiles.length).toBeGreaterThan(0); + + const [, content] = swiftFiles[0]; + expect(content).toContain('let equal ='); + expect(content).not.toContain('val equal ='); + }); + + it('should handle multiple val keywords in a file', async () => { + // Track written files + let writtenFiles: Record = {}; + const trackingFileSystemExecutor = createMockFileSystemExecutor({ + existsSync: (path) => { + return ( + path.includes('xcodebuild-mcp-template') || + path.includes('XcodeBuildMCP-iOS-Template') || + path.includes('/template') || + path.endsWith('template') || + path.includes('extracted') || + path.includes('/mock/template/path') + ); + }, + readFile: async (path) => { + // Simulate a Swift test file with multiple Kotlin 'val' keywords + if (path.includes('.swift')) { + return `val foo = 1 +val bar = 2 +val baz = XCTAssertEqual(a, b)`; + } + return 'template content with MyProject placeholder'; + }, + readdir: async () => [ + { name: 'MyProjectTests.swift', isDirectory: () => false, isFile: () => true } as any, + ], + mkdir: async () => {}, + rm: async () => {}, + cp: async () => {}, + writeFile: async (path, content) => { + writtenFiles[path] = content as string; + }, + stat: async () => ({ isDirectory: () => true, mtimeMs: 0 }), + }); + + await scaffold_ios_projectLogic( + { + projectName: 'TestApp', + customizeNames: true, + outputPath: '/tmp/test-projects', + }, + mockCommandExecutor, + trackingFileSystemExecutor, + ); + + // Verify that all 'val' keywords are replaced with 'let' + const swiftFiles = Object.entries(writtenFiles).filter(([path]) => path.endsWith('.swift')); + expect(swiftFiles.length).toBeGreaterThan(0); + + const [, content] = swiftFiles[0]; + expect(content).toContain('let foo ='); + expect(content).toContain('let bar ='); + expect(content).toContain('let baz ='); + expect(content).not.toContain('val '); + }); + }); }); diff --git a/src/mcp/tools/project-scaffolding/scaffold_ios_project.ts b/src/mcp/tools/project-scaffolding/scaffold_ios_project.ts index c98a43fe..619d0bc8 100644 --- a/src/mcp/tools/project-scaffolding/scaffold_ios_project.ts +++ b/src/mcp/tools/project-scaffolding/scaffold_ios_project.ts @@ -230,6 +230,19 @@ function updateXCConfigFile(content: string, params: Record): s return result; } +/** + * Fix Swift syntax issues (e.g., Kotlin 'val' keyword should be 'let') + */ +function fixSwiftSyntax(content: string): string { + let result = content; + + // Replace Kotlin 'val' keyword with Swift 'let' + // Match 'val' followed by whitespace and an identifier (variable name) + result = result.replace(/\bval\s+(\w+)\s*=/g, 'let $1 ='); + + return result; +} + /** * Replace placeholders in a string (for non-XCConfig files) */ @@ -301,6 +314,7 @@ async function processFile( const isTextFile = textExtensions.some((textExt) => ext.endsWith(textExt)); const isXCConfig = sourcePath.endsWith('.xcconfig'); const isPackageSwift = sourcePath.endsWith('Package.swift'); + const isSwiftFile = sourcePath.endsWith('.swift'); if (isTextFile && customizeNames) { // Read the file content @@ -322,6 +336,11 @@ async function processFile( processedContent = replacePlaceholders(content, projectName, bundleIdentifier); } + // Apply Swift syntax fixes if this is a Swift file + if (isSwiftFile) { + processedContent = fixSwiftSyntax(processedContent); + } + await fileSystemExecutor.mkdir(dirname(finalDestPath), { recursive: true }); await fileSystemExecutor.writeFile(finalDestPath, processedContent, 'utf-8'); } else { diff --git a/src/mcp/tools/project-scaffolding/scaffold_macos_project.ts b/src/mcp/tools/project-scaffolding/scaffold_macos_project.ts index cc7841bd..cb8467b4 100644 --- a/src/mcp/tools/project-scaffolding/scaffold_macos_project.ts +++ b/src/mcp/tools/project-scaffolding/scaffold_macos_project.ts @@ -145,6 +145,19 @@ function updateXCConfigFile( return result; } +/** + * Fix Swift syntax issues (e.g., Kotlin 'val' keyword should be 'let') + */ +function fixSwiftSyntax(content: string): string { + let result = content; + + // Replace Kotlin 'val' keyword with Swift 'let' + // Match 'val' followed by whitespace and an identifier (variable name) + result = result.replace(/\bval\s+(\w+)\s*=/g, 'let $1 ='); + + return result; +} + /** * Replace placeholders in a string (for non-XCConfig files) */ @@ -212,6 +225,7 @@ async function processFile( const isTextFile = textExtensions.some((textExt) => ext.endsWith(textExt)); const isXCConfig = sourcePath.endsWith('.xcconfig'); const isPackageSwift = sourcePath.endsWith('Package.swift'); + const isSwiftFile = sourcePath.endsWith('.swift'); if (isTextFile && params.customizeNames) { // Read the file content @@ -233,6 +247,11 @@ async function processFile( processedContent = replacePlaceholders(content, params.projectName, bundleIdentifier); } + // Apply Swift syntax fixes if this is a Swift file + if (isSwiftFile) { + processedContent = fixSwiftSyntax(processedContent); + } + await fileSystemExecutor.mkdir(dirname(finalDestPath), { recursive: true }); await fileSystemExecutor.writeFile(finalDestPath, processedContent, 'utf-8'); } else {