Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, string> = {};
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<string, string> = {};
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 ');
});
});
});
19 changes: 19 additions & 0 deletions src/mcp/tools/project-scaffolding/scaffold_ios_project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,19 @@ function updateXCConfigFile(content: string, params: Record<string, unknown>): 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;
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Duplicated fixSwiftSyntax function across scaffolding files

Low Severity

The fixSwiftSyntax function is duplicated identically in both scaffold_ios_project.ts and scaffold_macos_project.ts. Both implementations contain the same regex replacement logic (/\bval\s+(\w+)\s*=/glet $1 =). This duplication increases maintenance burden and risks inconsistent bug fixes if the logic needs to be updated in the future—a fix applied to one file might be forgotten in the other.

Additional Locations (1)

Fix in Cursor Fix in Web


/**
* Replace placeholders in a string (for non-XCConfig files)
*/
Expand Down Expand Up @@ -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
Expand All @@ -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);
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Swift syntax fix skipped when customizeNames is false

Low Severity

The fixSwiftSyntax call is inside the if (isTextFile && customizeNames) block. When a user scaffolds with customizeNames: false, Swift files are copied via cp in the else branch, completely bypassing the syntax fix. This means the Kotlin val syntax error would persist in the output files, defeating the purpose of the fix ("prevent compilation errors") for users who explicitly set customizeNames: false.

Additional Locations (1)

Fix in Cursor Fix in Web


await fileSystemExecutor.mkdir(dirname(finalDestPath), { recursive: true });
await fileSystemExecutor.writeFile(finalDestPath, processedContent, 'utf-8');
} else {
Expand Down
19 changes: 19 additions & 0 deletions src/mcp/tools/project-scaffolding/scaffold_macos_project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
*/
Expand Down Expand Up @@ -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
Expand All @@ -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 {
Expand Down
Loading