Skip to content

Commit debd3f2

Browse files
committed
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
1 parent a30ffe9 commit debd3f2

4 files changed

Lines changed: 149 additions & 0 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
- Update UI automation guard guidance to point at `debug_continue` when paused.
1616
- Fix tool loading bugs in static tool registration.
1717
- Fix xcodemake command argument corruption when project directory path appears as substring in non-path arguments.
18+
- Fixed Swift syntax error in scaffolded projects by replacing Kotlin 'val' keyword with Swift 'let' (Fixes XCODEBUILD-MCP-132X).
1819

1920
## [1.16.0] - 2025-12-30
2021
- Remove dynamic tool discovery (`discover_tools`) and `XCODEBUILDMCP_DYNAMIC_TOOLS`. Use `XCODEBUILDMCP_ENABLED_WORKFLOWS` to limit startup tool registration.

src/mcp/tools/project-scaffolding/__tests__/scaffold_ios_project.test.ts

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -666,4 +666,114 @@ describe('scaffold_ios_project plugin', () => {
666666
process.env.XCODEBUILDMCP_IOS_TEMPLATE_PATH = '/mock/template/path';
667667
});
668668
});
669+
670+
describe('Swift Syntax Fixes', () => {
671+
it('should fix Kotlin val keyword in Swift test files', async () => {
672+
// Track written files
673+
let writtenFiles: Record<string, string> = {};
674+
const trackingFileSystemExecutor = createMockFileSystemExecutor({
675+
existsSync: (path) => {
676+
return (
677+
path.includes('xcodebuild-mcp-template') ||
678+
path.includes('XcodeBuildMCP-iOS-Template') ||
679+
path.includes('/template') ||
680+
path.endsWith('template') ||
681+
path.includes('extracted') ||
682+
path.includes('/mock/template/path')
683+
);
684+
},
685+
readFile: async (path) => {
686+
// Simulate a Swift test file with Kotlin 'val' keyword
687+
if (path.includes('.swift')) {
688+
return 'val equal = XCTAssertEqual(actual, expected, message, file: file, line: line)';
689+
}
690+
return 'template content with MyProject placeholder';
691+
},
692+
readdir: async () => [
693+
{ name: 'MyProjectTests.swift', isDirectory: () => false, isFile: () => true } as any,
694+
],
695+
mkdir: async () => {},
696+
rm: async () => {},
697+
cp: async () => {},
698+
writeFile: async (path, content) => {
699+
writtenFiles[path] = content as string;
700+
},
701+
stat: async () => ({ isDirectory: () => true, mtimeMs: 0 }),
702+
});
703+
704+
await scaffold_ios_projectLogic(
705+
{
706+
projectName: 'TestApp',
707+
customizeNames: true,
708+
outputPath: '/tmp/test-projects',
709+
},
710+
mockCommandExecutor,
711+
trackingFileSystemExecutor,
712+
);
713+
714+
// Verify that the written Swift file has 'let' instead of 'val'
715+
const swiftFiles = Object.entries(writtenFiles).filter(([path]) => path.endsWith('.swift'));
716+
expect(swiftFiles.length).toBeGreaterThan(0);
717+
718+
const [, content] = swiftFiles[0];
719+
expect(content).toContain('let equal =');
720+
expect(content).not.toContain('val equal =');
721+
});
722+
723+
it('should handle multiple val keywords in a file', async () => {
724+
// Track written files
725+
let writtenFiles: Record<string, string> = {};
726+
const trackingFileSystemExecutor = createMockFileSystemExecutor({
727+
existsSync: (path) => {
728+
return (
729+
path.includes('xcodebuild-mcp-template') ||
730+
path.includes('XcodeBuildMCP-iOS-Template') ||
731+
path.includes('/template') ||
732+
path.endsWith('template') ||
733+
path.includes('extracted') ||
734+
path.includes('/mock/template/path')
735+
);
736+
},
737+
readFile: async (path) => {
738+
// Simulate a Swift test file with multiple Kotlin 'val' keywords
739+
if (path.includes('.swift')) {
740+
return `val foo = 1
741+
val bar = 2
742+
val baz = XCTAssertEqual(a, b)`;
743+
}
744+
return 'template content with MyProject placeholder';
745+
},
746+
readdir: async () => [
747+
{ name: 'MyProjectTests.swift', isDirectory: () => false, isFile: () => true } as any,
748+
],
749+
mkdir: async () => {},
750+
rm: async () => {},
751+
cp: async () => {},
752+
writeFile: async (path, content) => {
753+
writtenFiles[path] = content as string;
754+
},
755+
stat: async () => ({ isDirectory: () => true, mtimeMs: 0 }),
756+
});
757+
758+
await scaffold_ios_projectLogic(
759+
{
760+
projectName: 'TestApp',
761+
customizeNames: true,
762+
outputPath: '/tmp/test-projects',
763+
},
764+
mockCommandExecutor,
765+
trackingFileSystemExecutor,
766+
);
767+
768+
// Verify that all 'val' keywords are replaced with 'let'
769+
const swiftFiles = Object.entries(writtenFiles).filter(([path]) => path.endsWith('.swift'));
770+
expect(swiftFiles.length).toBeGreaterThan(0);
771+
772+
const [, content] = swiftFiles[0];
773+
expect(content).toContain('let foo =');
774+
expect(content).toContain('let bar =');
775+
expect(content).toContain('let baz =');
776+
expect(content).not.toContain('val ');
777+
});
778+
});
669779
});

src/mcp/tools/project-scaffolding/scaffold_ios_project.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,19 @@ function updateXCConfigFile(content: string, params: Record<string, unknown>): s
230230
return result;
231231
}
232232

233+
/**
234+
* Fix Swift syntax issues (e.g., Kotlin 'val' keyword should be 'let')
235+
*/
236+
function fixSwiftSyntax(content: string): string {
237+
let result = content;
238+
239+
// Replace Kotlin 'val' keyword with Swift 'let'
240+
// Match 'val' followed by whitespace and an identifier (variable name)
241+
result = result.replace(/\bval\s+(\w+)\s*=/g, 'let $1 =');
242+
243+
return result;
244+
}
245+
233246
/**
234247
* Replace placeholders in a string (for non-XCConfig files)
235248
*/
@@ -301,6 +314,7 @@ async function processFile(
301314
const isTextFile = textExtensions.some((textExt) => ext.endsWith(textExt));
302315
const isXCConfig = sourcePath.endsWith('.xcconfig');
303316
const isPackageSwift = sourcePath.endsWith('Package.swift');
317+
const isSwiftFile = sourcePath.endsWith('.swift');
304318

305319
if (isTextFile && customizeNames) {
306320
// Read the file content
@@ -322,6 +336,11 @@ async function processFile(
322336
processedContent = replacePlaceholders(content, projectName, bundleIdentifier);
323337
}
324338

339+
// Apply Swift syntax fixes if this is a Swift file
340+
if (isSwiftFile) {
341+
processedContent = fixSwiftSyntax(processedContent);
342+
}
343+
325344
await fileSystemExecutor.mkdir(dirname(finalDestPath), { recursive: true });
326345
await fileSystemExecutor.writeFile(finalDestPath, processedContent, 'utf-8');
327346
} else {

src/mcp/tools/project-scaffolding/scaffold_macos_project.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,19 @@ function updateXCConfigFile(
145145
return result;
146146
}
147147

148+
/**
149+
* Fix Swift syntax issues (e.g., Kotlin 'val' keyword should be 'let')
150+
*/
151+
function fixSwiftSyntax(content: string): string {
152+
let result = content;
153+
154+
// Replace Kotlin 'val' keyword with Swift 'let'
155+
// Match 'val' followed by whitespace and an identifier (variable name)
156+
result = result.replace(/\bval\s+(\w+)\s*=/g, 'let $1 =');
157+
158+
return result;
159+
}
160+
148161
/**
149162
* Replace placeholders in a string (for non-XCConfig files)
150163
*/
@@ -212,6 +225,7 @@ async function processFile(
212225
const isTextFile = textExtensions.some((textExt) => ext.endsWith(textExt));
213226
const isXCConfig = sourcePath.endsWith('.xcconfig');
214227
const isPackageSwift = sourcePath.endsWith('Package.swift');
228+
const isSwiftFile = sourcePath.endsWith('.swift');
215229

216230
if (isTextFile && params.customizeNames) {
217231
// Read the file content
@@ -233,6 +247,11 @@ async function processFile(
233247
processedContent = replacePlaceholders(content, params.projectName, bundleIdentifier);
234248
}
235249

250+
// Apply Swift syntax fixes if this is a Swift file
251+
if (isSwiftFile) {
252+
processedContent = fixSwiftSyntax(processedContent);
253+
}
254+
236255
await fileSystemExecutor.mkdir(dirname(finalDestPath), { recursive: true });
237256
await fileSystemExecutor.writeFile(finalDestPath, processedContent, 'utf-8');
238257
} else {

0 commit comments

Comments
 (0)