Skip to main content

Analyzer Plugin

Ormed ships an optional analyzer plugin (no separate package) that inspects your query builder usage, DTOs, and model metadata during analysis. It helps catch unsafe patterns, invalid field names, and other common pitfalls before runtime.

Enable the plugin

  1. Add ormed to dev_dependencies.
  2. Enable the plugin in your project's analysis_options.yaml:
analyzer:
plugins:
- ormed

After changing analysis_options.yaml, restart the Dart Analysis Server. If you previously used the standalone ormed_analyzer package, remove it from your pubspec.yaml and update analysis_options.yaml to use ormed.

How it works

  • The plugin scans generated *.orm.dart files to build a model index. Run build_runner first so the definitions exist.
  • Query modifiers are tracked within the same function body, including split chains and cascades:
    final q = query.where('email', '=', 'a@example.com');
    q.get(); // no warning because the earlier where() is tracked
  • The tracker is intra-procedural (no cross-function tracking) and does not model control-flow branches precisely.
  • Only literal field names can be validated. Dynamic strings are ignored.

Diagnostics (grouped)

Field and selection validation:

  • ormed_unknown_field: unknown field/column in query builder calls.
Model.query<Post>()
.where('missing_field', true); // ormed_unknown_field
  • ormed_unknown_select_field: unknown column in select([...]).
Model.query<Post>()
.select(['missing_select']); // ormed_unknown_select_field
  • ormed_duplicate_select_field: duplicate column in select([...]).
Model.query<Post>()
.select(['title', 'title']); // ormed_duplicate_select_field
  • ormed_unknown_order_field: unknown column in orderBy(...).
Model.query<Post>()
.orderBy('missing_order'); // ormed_unknown_order_field
  • ormed_unknown_group_field: unknown column in groupBy(...).
Model.query<Post>()
.groupBy(['missing_group']); // ormed_unknown_group_field
  • ormed_unknown_having_field: unknown column in having(...).
Model.query<Post>()
.having('missing_having', PredicateOperator.equals, 1); // ormed_unknown_having_field

Relation validation:

  • ormed_unknown_relation: unknown relation in withRelation(...) or similar.
Model.query<Post>()
.withRelation('missingRelation'); // ormed_unknown_relation
  • ormed_unknown_nested_relation: unknown nested relation path.
Model.query<Post>()
.withRelation('comments.missingRelation'); // ormed_unknown_nested_relation
  • ormed_invalid_where_has: whereHas(...) targets a missing relation.
Model.query<Post>()
.whereHas('missingRelation'); // ormed_invalid_where_has
  • ormed_relation_field_mismatch: relation callback uses a field from the wrong model.
Model.query<Post>()
.whereHas('comments', (q) => q.where('email', 'oops')); // ormed_relation_field_mismatch
  • ormed_missing_pivot_field: missing pivot field in many-to-many definitions.
Model.query<Post>()
.withRelation(const RelationDefinition(name: 'tags', kind: RelationKind.manyToMany, targetModel: 'Tag', pivotColumns: ['missing_pivot'], pivotModel: 'PostTag')); // ormed_missing_pivot_field

Type-aware predicate checks:

  • ormed_type_mismatch_eq: whereEquals(...) value type mismatches the field.
Model.query<Post>()
.whereEquals('userId', 'not_an_int'); // ormed_type_mismatch_eq
  • ormed_where_in_type_mismatch: whereIn(...) values mismatch the field type.
Model.query<Post>()
.whereIn('userId', ['not_an_int']); // ormed_where_in_type_mismatch
  • ormed_where_between_type_mismatch: whereBetween(...) values mismatch the field type.
Model.query<Post>()
.whereBetween('userId', 'a', 'z'); // ormed_where_between_type_mismatch
  • ormed_typed_predicate_field: typed predicate field does not exist on the model.
Model.query<Post>()
.whereTyped((q) => q.legacy.eq('oops')); // ormed_typed_predicate_field

Query safety and performance:

  • ormed_update_delete_without_where: update() or delete() without constraints.
Model.query<Post>()
.update({'title': 'updated'}); // ormed_update_delete_without_where
Model.query<Post>()
.delete(); // ormed_update_delete_without_where
  • ormed_offset_without_order: offset() without orderBy.
Model.query<Post>()
.offset(10); // ormed_offset_without_order
  • ormed_limit_without_order: limit() without orderBy.
Model.query<Post>()
.limit(10); // ormed_limit_without_order
  • ormed_get_without_limit: get(), rows(), getPartial(), Model.all(), ModelCompanion.all(), or generated companion Posts.all() used without a limit() or chunk/paginate alternative.
Model.query<Post>()
.get(); // ormed_get_without_limit
Posts.all(); // ormed_get_without_limit

Raw SQL safety:

  • ormed_raw_sql_interpolation: raw SQL with string interpolation and no bindings.
Model.query<Post>()
.whereRaw('title = $title'); // ormed_raw_sql_interpolation
  • ormed_raw_sql_alias_missing: selectRaw(...) without an alias.
Model.query<Post>()
.selectRaw('count(*)'); // ormed_raw_sql_alias_missing

DTO validation:

  • ormed_insert_missing_required: insert DTO missing required fields.
Model.repository<Post>()
.insert(PostInsertDto()); // ormed_insert_missing_required
  • ormed_update_missing_pk: update DTO missing a primary key (or missing where).
Model.repository<Post>()
.update(PostUpdateDto(title: 'updated')); // ormed_update_missing_pk

Soft delete and timestamp checks:

  • ormed_with_trashed_on_non_soft_delete
Model.query<User>()
.withTrashed(); // ormed_with_trashed_on_non_soft_delete
  • ormed_without_timestamps_on_timestamped_model
User(email: 'a@b.com', name: 'Test')
.withoutTimestamps(() {}); // ormed_without_timestamps_on_timestamped_model
  • ormed_updated_at_access_on_without_timestamps
Tag(id: 1, name: 'ormed')
.updatedAt; // ormed_updated_at_access_on_without_timestamps

Suppressing diagnostics

Suppress a single diagnostic with:

// ignore: ormed/ormed_unknown_field

Or suppress an entire file:

// ignore_for_file: ormed/ormed_get_without_limit

Generated code warnings

The plugin analyzes generated .orm.dart files by default (it needs them to build the model index). If you do not want warnings in generated files, add excludes to analysis_options.yaml:

analyzer:
exclude:
- "**/*.g.dart"
- "**/*.orm.dart"

AOT snapshot workaround

If your project uses ormed_sqlite, the analyzer plugin may fail to compile an AOT snapshot because ormed_sqlite depends on build hooks. In that case, run analysis with:

dart analyze --no-use-aot-snapshot

Example

final users = await context.query<User>()
.where('emali', 'a@b.com'); // flagged by the plugin