garrytan/gstack · error · AccessorGenerationError

gen-accessors: invalid @Snapshotable declaration(s): ${diagn

Error message

gen-accessors: invalid @Snapshotable declaration(s):
${diagnostics.map(d => `  - ${d}`).join('\n')}

What it means

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.

Source

Thrown at ios-qa/scripts/gen-accessors.ts:145

    if (endIdx === -1) continue;
    const body = masked.slice(startIdx, endIdx);

    const parsed = parseFields(body, source, startIdx, className);
    if (braceDepthAt(masked, matchOffset) !== 0) {
      if (parsed.fields.length > 0 || parsed.diagnostics.length > 0) {
        const line = source.slice(0, matchOffset).split(/\r?\n/).length;
        diagnostics.push(`${className} (line ${line}): nested @Observable types are not supported; move the type to file scope`);
      }
      continue;
    }
    diagnostics.push(...parsed.diagnostics);
    const fields = parsed.fields;
    if (fields.length > 0) {
      specs.push({ className, fields });
    }
  }

  if (diagnostics.length > 0) throw new AccessorGenerationError(diagnostics);
  return specs;
}

function braceDepthAt(masked: string, offset: number): number {
  let depth = 0;
  for (let i = 0; i < offset; i++) {
    if (masked[i] === '{') depth++;
    else if (masked[i] === '}') depth = Math.max(0, depth - 1);
  }
  return depth;
}

function findMatchingBrace(s: string, openIdx: number): number {
  // Strings and comments have already been blanked by maskSwiftSource, so
  // braces here are syntax rather than prose or literal content.
  let depth = 0;
  for (let i = openIdx; i < s.length; i++) {
    const c = s[i];

View on GitHub (pinned to 94993f7401)

Solutions

  1. Read each diagnostic line — it names the class, line number, and exact problem.
  2. For nested-@Observable errors, move the inner class to file scope.
  3. For unsupported-type errors, change the field to a JSON-scalar/array/String-keyed-dictionary type or remove @Snapshotable.
  4. Re-run /ios-sync to confirm diagnostics clear.

Example fix

// before — nested @Observable
class Outer {
  @Observable class Inner { @Snapshotable var x: Int }
}

// after — move to file scope
@Observable class Inner { @Snapshotable var x: Int }
Defensive patterns

Strategy: validation

Validate before calling

// Before running gen-accessors, lint @Snapshotable usage
function lintSnapshotableDeclarations(source: string): string[] {
  const diagnostics: string[] = [];
  // detect nested @Observable, unsupported types, malformed markers
  return diagnostics;
}

Try / catch

try {
  parseSwift(source);
} catch (e) {
  if (e instanceof AccessorGenerationError) {
    console.error('Fix these @Snapshotable issues then re-run /ios-sync:');
    e.diagnostics.forEach(d => console.error('  - ' + d));
    process.exit(1);
  }
  throw e;
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of garrytan/gstack@94993f7401 (2026-08-12). Data as JSON: /api/errors/2d3b62b5910ac9c8. Report an issue: GitHub.