Everything Stack Analyzer

Custom Dart analyzer plugin that enforces schema evolution rules for the Everything Stack framework.

This plugin is the correctness gate - it prevents developers from accidentally breaking schema compatibility through static analysis.

Why This Matters

The Everything Stack uses versioning and snapshots to handle schema evolution safely. But the infrastructure is useless if developers can accidentally:

  • Add required fields to existing entities (breaks old snapshots)
  • Add new fields without defaults (deserialization fails)
  • Change field types (silent data loss)

This analyzer enforces the safe patterns, preventing these violations before code is committed.

Lints

1. require_field_on_new_entity

Severity: Warning Pattern: required field without default on entity classes

Problem: When you add a required field to an entity, old snapshots won't have that field. Deserialization will fail.

Example (BAD):

class Note extends BaseEntity {
  String title;       // v1
  String content;     // v1
  List<String> tags;  // v2 - REQUIRED! Old snapshots don't have this!
}

Fix:

class Note extends BaseEntity {
  String title;
  String content;
  List<String>? tags;  // v2 - Optional, defaults to null for old snapshots
}

Or provide a default:

List<String> tags = [];  // Default value provided

2. new_field_without_default_or_optional

Severity: Warning Pattern: New field without JSON deserialization default in @JsonSerializable classes

Problem: When deserializing old snapshots, new fields won't exist in the JSON. Without a default value, the field becomes null unsafely.

Example (BAD):

@JsonSerializable()
class Note extends BaseEntity {
  String title;
  List<String> tags;  // NEW - missing from old snapshots!
}

When deserializing a v1 snapshot:

{
  "title": "Task",
  "content": "Do something"
  // No "tags" field!
}

The tags field will be uninitialized, violating null safety.

Fix - Option 1: Use @JsonKey with default:

@JsonSerializable()
class Note extends BaseEntity {
  String title;

  @JsonKey(defaultValue: [])
  List<String> tags;  // Default when missing from JSON
}

Fix - Option 2: Make optional:

@JsonSerializable()
class Note extends BaseEntity {
  String title;
  List<String>? tags;  // Null when missing from JSON
}

3. field_type_change_detection

Severity: Info Pattern: Potential type mismatches on fields

Problem: Changing a field's type (e.g., Stringint) breaks backward compatibility. Old snapshots have the old type.

Example (BAD):

// v1 schema
class Note {
  String priority;  // "high", "medium", "low"
}

// v2 schema - someone changes to int
class Note {
  int priority;  // BREAKING! Old snapshots have String
}

Fix: Add a new field and deprecate the old one:

class Note {
  @Deprecated('Use priorityLevel instead')
  String? priority;

  @JsonKey(defaultValue: 0)
  int priorityLevel;  // New field with safe default
}

Installation

Step 1: Add to pubspec.yaml

In your main project's pubspec.yaml:

dev_dependencies:
  everything_stack_analyzer:
    path: ../everything_stack_analyzer

Step 2: Enable in analysis_options.yaml

Add to analysis_options.yaml:

analyzer:
  plugins:
    - custom_lint

linter:
  rules:
    # Schema evolution rules (enforced by everything_stack_analyzer)
    - require_field_on_new_entity
    - new_field_without_default_or_optional
    - field_type_change_detection

Step 3: Run analyzer

flutter analyze

All three lints will run automatically.


How It Works

Architecture

The analyzer is built on:

  • custom_lint - Dart's official plugin system for custom lint rules
  • analyzer - The Dart analyzer AST for static code analysis

Each lint rule:

  1. Walks the AST - Examines class declarations, fields, annotations
  2. Detects patterns - Looks for unsafe schema evolution patterns
  3. Reports errors - Highlights violations with actionable messages

Rule Implementation

Example: require_field_on_new_entity

class RequiredFieldOnNewEntity extends DartLintRule {
  @override
  void run(CustomLintResolver resolver, ErrorReporter reporter, CustomLintContext context) {
    // 1. Find all classes that extend BaseEntity
    context.registry.addClassDeclaration((node) {
      if (!_extendsBaseEntity(node)) return;

      // 2. Check each field
      for (final field in node.members.whereType<FieldDeclaration>()) {
        // 3. Flag non-nullable fields without defaults
        if (field.fields.type?.question == null && !_hasDefault(field)) {
          reporter.reportErrorForNode(code, field, [...]);
        }
      }
    });
  }
}

The analyzer:

  • ✅ Detects classes extending BaseEntity
  • ✅ Checks field nullability (?)
  • ✅ Checks for default values
  • ✅ Reports violations with fix suggestions

Schema Evolution Guide

Safe patterns this analyzer enforces:

✅ SAFE: Add optional field

// v1
class Note {
  String title;
}

// v2
class Note {
  String title;
  List<String>? tags;  // Optional - old snapshots default to null
}
  • v1 snapshot loads: tags = null
  • v2 snapshot loads: tags = ['tag1', 'tag2']
  • Old app loads v2 snapshot: tags is ignored ✅

✅ SAFE: Add field with default

class Note {
  String title;

  @JsonKey(defaultValue: [])
  List<String> tags;  // Default provided
}
  • Old snapshots missing tags? Use []
  • New snapshots have tags? Use the value

❌ UNSAFE: Add required field

class Note {
  String title;
  List<String> tags;  // REQUIRED - old snapshots crash!
}

Old snapshots:

{"title": "Task", "content": "..."}
// No tags field!

Deserialization fails → Entity can't be loaded.

❌ UNSAFE: Change field type

// v1
class Note {
  String priority;  // "high", "medium", "low"
}

// v2
class Note {
  int priority;  // BREAKING! Type mismatch
}

Deserialization fails → Type error.

✅ SAFE: Migrate field type

class Note {
  @Deprecated('Use priorityLevel instead')
  String? priority;  // Keep old field for backward compat

  @JsonKey(defaultValue: 0)
  int priorityLevel;  // New field with safe default
}
  • Old snapshots load: priority set, priorityLevel = 0
  • New app migrates data: Convert prioritypriorityLevel
  • New snapshots only have priorityLevel

Testing

Run tests to verify the analyzer works:

cd everything_stack_analyzer
dart pub get
dart pub run build_runner build  # Build custom lints
dart test

Or from the main project:

flutter analyze --ignore-infos

Future Enhancements

Git-based detection

Track schema changes across commits:

// Detects this as a breaking change
class FieldTypeChangeDetection extends DartLintRule {
  // Git integration to detect:
  // - Type changes on fields
  // - Removed required fields
  // - Non-backward-compatible migrations
}

Migration framework

Auto-generate migrations:

// Generates migration code
@Migration(from: 1, to: 2)
class AddTagsField {
  static void migrate(Map<String, dynamic> entity) {
    entity['tags'] = [];  // Safe default
  }
}

Schema documentation

Link violations to documentation:

See: docs/schema-evolution.md#required-fields

Troubleshooting

Analyzer not running?

  1. Check pubspec.yaml:

    analyzer:
      plugins:
        - custom_lint
    
  2. Clear build cache:

    flutter clean
    pub cache repair
    
  3. Rebuild:

    flutter analyze
    

Too many warnings?

Disable specific rules in analysis_options.yaml:

linter:
  rules:
    - require_field_on_new_entity: false  # Disable if not needed

False positives?

Suppress for specific lines:

class Note extends BaseEntity {
  // ignore: require_field_on_new_entity
  List<String> tags;  // Known-safe pattern
}

Contributing

To add new rules:

  1. Create a new DartLintRule in lib/src/lints/
  2. Implement the run() method with AST traversal
  3. Add tests in test/
  4. Update lib/everything_stack_analyzer.dart to register the rule
  5. Document in this README

References