{"record":{"id":"2d3b62b5910ac9c8","repo":"garrytan/gstack","slug":"gen-accessors-invalid-snapshotable-declaration-s","errorCode":null,"errorMessage":"gen-accessors: invalid @Snapshotable declaration(s):\n${diagnostics.map(d => `  - ${d}`).join('\\n')}","messagePattern":"gen-accessors: invalid @Snapshotable declaration\\(s\\):\n(.+?)`\\)\\.join\\('\\\\n'\\)\\}","errorType":"validation","errorClass":"AccessorGenerationError","httpStatus":null,"severity":"error","filePath":"ios-qa/scripts/gen-accessors.ts","lineNumber":145,"sourceCode":"    if (endIdx === -1) continue;\n    const body = masked.slice(startIdx, endIdx);\n\n    const parsed = parseFields(body, source, startIdx, className);\n    if (braceDepthAt(masked, matchOffset) !== 0) {\n      if (parsed.fields.length > 0 || parsed.diagnostics.length > 0) {\n        const line = source.slice(0, matchOffset).split(/\\r?\\n/).length;\n        diagnostics.push(`${className} (line ${line}): nested @Observable types are not supported; move the type to file scope`);\n      }\n      continue;\n    }\n    diagnostics.push(...parsed.diagnostics);\n    const fields = parsed.fields;\n    if (fields.length > 0) {\n      specs.push({ className, fields });\n    }\n  }\n\n  if (diagnostics.length > 0) throw new AccessorGenerationError(diagnostics);\n  return specs;\n}\n\nfunction braceDepthAt(masked: string, offset: number): number {\n  let depth = 0;\n  for (let i = 0; i < offset; i++) {\n    if (masked[i] === '{') depth++;\n    else if (masked[i] === '}') depth = Math.max(0, depth - 1);\n  }\n  return depth;\n}\n\nfunction findMatchingBrace(s: string, openIdx: number): number {\n  // Strings and comments have already been blanked by maskSwiftSource, so\n  // braces here are syntax rather than prose or literal content.\n  let depth = 0;\n  for (let i = openIdx; i < s.length; i++) {\n    const c = s[i];","sourceCodeStart":127,"sourceCodeEnd":163,"githubUrl":"https://github.com/garrytan/gstack/blob/94993f74012782fd94416dd44b8314f6363a13a4/ios-qa/scripts/gen-accessors.ts#L127-L163","documentation":"AccessorGenerationError thrown by parseSwift() when scanning @Observable Swift classes for @Snapshotable fields yields any diagnostics. It collects every problem (nested @Observable types, fields with unsupported snapshot types, malformed @Snapshotable markers) and reports them in one batch so the developer can fix the source in a single pass rather than re-running per error. Generation halts because emitted accessors would not compile against invalid declarations.","triggerScenarios":"Running /ios-sync (gen-accessors) over Swift sources where: an @Observable class is nested inside another type; a @Snapshotable field uses a non-JSON-serializable type (e.g. a custom struct, closure, non-String-keyed dictionary); a @Snapshotable comment marker is malformed; a snapshot type is unsupported.","commonSituations":"Refactor that moved an @Observable class inside a parent type; a developer annotated a non-serializable property with @Snapshotable; a copy-paste left a malformed marker comment; Swift generic or enum-typed field marked as snapshotable.","solutions":["Read each diagnostic line — it names the class, line number, and exact problem.","For nested-@Observable errors, move the inner class to file scope.","For unsupported-type errors, change the field to a JSON-scalar/array/String-keyed-dictionary type or remove @Snapshotable.","Re-run /ios-sync to confirm diagnostics clear."],"exampleFix":"// before — nested @Observable\nclass Outer {\n  @Observable class Inner { @Snapshotable var x: Int }\n}\n\n// after — move to file scope\n@Observable class Inner { @Snapshotable var x: Int }","handlingStrategy":"validation","validationCode":"// Before running gen-accessors, lint @Snapshotable usage\nfunction lintSnapshotableDeclarations(source: string): string[] {\n  const diagnostics: string[] = [];\n  // detect nested @Observable, unsupported types, malformed markers\n  return diagnostics;\n}","typeGuard":null,"tryCatchPattern":"try {\n  parseSwift(source);\n} catch (e) {\n  if (e instanceof AccessorGenerationError) {\n    console.error('Fix these @Snapshotable issues then re-run /ios-sync:');\n    e.diagnostics.forEach(d => console.error('  - ' + d));\n    process.exit(1);\n  }\n  throw e;\n}","preventionTips":["Keep @Observable classes at file scope; never nest them.","Only mark JSON-serializable fields (scalar/array/String-keyed dict) with @Snapshotable.","Run /ios-sync in CI to catch declaration errors before merge.","Use the canonical `// @Snapshotable` comment marker exactly as documented."],"tags":["ios-qa","codegen","swift","snapshot","validation"],"backgroundTag":null,"analyzedSha":"94993f74012782fd94416dd44b8314f6363a13a4","analyzedAt":"2026-08-12T04:06:23.140Z","schemaVersion":2},"datasetVersion":"2026-08-12T12:31:55.035Z"}