everything_stack_analyzer 0.1.0
everything_stack_analyzer: ^0.1.0 copied to clipboard
Analyzer plugin for Everything Stack - enforces schema evolution rules and best practices.
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
References #
- Dart Custom Lint
- Analyzer AST
- Schema Evolution Design
- Everything Stack Architecture