Skip to content

Commit 3813717

Browse files
masonwyatt23claude
andcommitted
fix: resolve real-world Swift compilation issues
Testing against TourVault (164-file app) reduced errors from 2451 to 106: - SwiftData @model 'description' field conflict → renamed to 'descriptionText' - 'Unknown' TypeScript type leaking into Swift → filtered in sanitizer - NetworkError.unknown reference → updated to APIError.networkError - SwiftData init(from:)/toModel() handles Data↔Dictionary conversion Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 2d50fdf commit 3813717

3 files changed

Lines changed: 24 additions & 16 deletions

File tree

src/generator/model-generator.ts

Lines changed: 21 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -643,6 +643,11 @@ function generateSwiftDataModel(entity: Entity, model: SemanticAppModel): string
643643
const relationshipFieldNames = new Set(relationships.map(r => camelCase(r.fieldName)));
644644
const lines: string[] = [];
645645

646+
// SwiftData @Model macro conflicts with property named 'description' (CustomStringConvertible)
647+
// Rename to 'desc' in the SwiftData model and handle conversion in init/toModel
648+
const SWIFTDATA_RESERVED = new Set(['description', 'hash']);
649+
const sdFieldName = (name: string) => SWIFTDATA_RESERVED.has(name) ? `${name}Text` : name;
650+
646651
lines.push('@Model');
647652
lines.push(`final class ${storeName} {`);
648653
if (!hasId) { lines.push(' var id: UUID'); lines.push(''); }
@@ -661,7 +666,7 @@ function generateSwiftDataModel(entity: Entity, model: SemanticAppModel): string
661666
continue;
662667
}
663668
}
664-
lines.push(` var ${field.name}: ${swiftDataFieldType(field.type)}`);
669+
lines.push(` var ${sdFieldName(field.name)}: ${swiftDataFieldType(field.type)}`);
665670
}
666671

667672
// Init
@@ -671,15 +676,17 @@ function generateSwiftDataModel(entity: Entity, model: SemanticAppModel): string
671676
for (const field of fields) {
672677
if (relationshipFieldNames.has(field.name)) continue;
673678
const sdType = swiftDataFieldType(field.type);
674-
if (field.isOptional) initParams.push(`${field.name}: ${sdType} = nil`);
675-
else if (field.isId && sdType === 'UUID') initParams.push(`${field.name}: ${sdType} = UUID()`);
676-
else initParams.push(`${field.name}: ${sdType}`);
679+
const pName = sdFieldName(field.name);
680+
if (field.isOptional) initParams.push(`${pName}: ${sdType} = nil`);
681+
else if (field.isId && sdType === 'UUID') initParams.push(`${pName}: ${sdType} = UUID()`);
682+
else initParams.push(`${pName}: ${sdType}`);
677683
}
678684
lines.push(` init(${initParams.join(', ')}) {`);
679685
if (!hasId) lines.push(' self.id = id');
680686
for (const field of fields) {
681687
if (relationshipFieldNames.has(field.name)) continue;
682-
lines.push(` self.${field.name} = ${field.name}`);
688+
const pName = sdFieldName(field.name);
689+
lines.push(` self.${pName} = ${pName}`);
683690
}
684691
lines.push(' }');
685692

@@ -690,19 +697,18 @@ function generateSwiftDataModel(entity: Entity, model: SemanticAppModel): string
690697
if (!hasId) convArgs.push('id: model.id');
691698
for (const field of fields) {
692699
if (relationshipFieldNames.has(field.name)) continue;
700+
const pName = sdFieldName(field.name);
693701
const sdType = swiftDataFieldType(field.type);
694702
if (sdType.replace('?', '') === 'Data' && field.type.replace('?', '') !== 'Data') {
695-
// Type needs JSON encoding: [String: String] → Data
696703
if (field.isOptional) {
697-
convArgs.push(`${field.name}: (try? JSONEncoder().encode(model.${field.name})) ?? nil`);
704+
convArgs.push(`${pName}: (try? JSONEncoder().encode(model.${field.name})) ?? nil`);
698705
} else {
699-
convArgs.push(`${field.name}: (try? JSONEncoder().encode(model.${field.name})) ?? Data()`);
706+
convArgs.push(`${pName}: (try? JSONEncoder().encode(model.${field.name})) ?? Data()`);
700707
}
701708
} else if (sdType.replace('?', '') === 'String' && field.type.replace('?', '') !== 'String' && !field.type.startsWith('[')) {
702-
// Non-string type stored as String
703-
convArgs.push(`${field.name}: String(describing: model.${field.name})`);
709+
convArgs.push(`${pName}: String(describing: model.${field.name})`);
704710
} else {
705-
convArgs.push(`${field.name}: model.${field.name}`);
711+
convArgs.push(`${pName}: model.${field.name}`);
706712
}
707713
}
708714
lines.push(` self.init(${convArgs.join(', ')})`);
@@ -715,16 +721,16 @@ function generateSwiftDataModel(entity: Entity, model: SemanticAppModel): string
715721
if (!hasId) modelArgs.push('id: id');
716722
for (const field of fields) {
717723
if (relationshipFieldNames.has(field.name)) continue;
724+
const pName = sdFieldName(field.name);
718725
const sdType = swiftDataFieldType(field.type);
719726
if (sdType.replace('?', '') === 'Data' && field.type.replace('?', '') !== 'Data') {
720-
// Data → decode back to original type
721727
if (field.isOptional) {
722-
modelArgs.push(`${field.name}: ${field.name}.flatMap { try? JSONDecoder().decode(${field.type.replace('?', '')}.self, from: $0) }`);
728+
modelArgs.push(`${field.name}: ${pName}.flatMap { try? JSONDecoder().decode(${field.type.replace('?', '')}.self, from: $0) }`);
723729
} else {
724-
modelArgs.push(`${field.name}: (try? JSONDecoder().decode(${field.type}.self, from: ${field.name})) ?? ${field.type}()`);
730+
modelArgs.push(`${field.name}: (try? JSONDecoder().decode(${field.type}.self, from: ${pName})) ?? ${field.type}()`);
725731
}
726732
} else {
727-
modelArgs.push(`${field.name}: ${field.name}`);
733+
modelArgs.push(`${field.name}: ${pName}`);
728734
}
729735
}
730736
lines.push(` ${structName}(${modelArgs.join(', ')})`);

src/generator/project-generator.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -270,7 +270,7 @@ function generateDefaultFetchMethod(storeName: string, model?: SemanticAppModel)
270270
lines.push(' } catch let networkError as NetworkError {');
271271
lines.push(' error = networkError');
272272
lines.push(' } catch {');
273-
lines.push(' self.error = .unknown(error)');
273+
lines.push(' self.error = .networkError(URLError(.unknown))');
274274
lines.push(' }');
275275
} else {
276276
// No matching API endpoint — generate a placeholder that compiles

src/generator/swiftui-generator.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,8 @@ function sanitizeSwiftTypeName(name: string | undefined): string | undefined {
259259
if (/[<>|{}]|=>/.test(name)) return undefined;
260260
// Reject leaked import paths (Prisma enums, relative paths, node_modules)
261261
if (/Import\(|import\(|node_modules|\.prisma|\.\/|\.\.\/|\$Enums/.test(name)) return undefined;
262+
// Reject TypeScript types that shouldn't appear in Swift
263+
if (name === 'Unknown' || name === 'unknown' || name === 'Partial' || name === 'Required' || name === 'Readonly') return undefined;
262264
return name;
263265
}
264266

0 commit comments

Comments
 (0)