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
requiredfields 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., String → int) 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 rulesanalyzer- The Dart analyzer AST for static code analysis
Each lint rule:
- Walks the AST - Examines class declarations, fields, annotations
- Detects patterns - Looks for unsafe schema evolution patterns
- 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:
tagsis 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:
priorityset,priorityLevel = 0 - New app migrates data: Convert
priority→priorityLevel - 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?
-
Check pubspec.yaml:
analyzer: plugins: - custom_lint -
Clear build cache:
flutter clean pub cache repair -
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:
- Create a new
DartLintRuleinlib/src/lints/ - Implement the
run()method with AST traversal - Add tests in
test/ - Update
lib/everything_stack_analyzer.dartto register the rule - Document in this README