diff --git a/skills/accessibility/references/examples.md b/skills/accessibility/references/examples.md index 1afb026..e45115c 100644 --- a/skills/accessibility/references/examples.md +++ b/skills/accessibility/references/examples.md @@ -15,16 +15,11 @@ import 'package:flutter/material.dart'; /// A rating bar that provides a single semantic description /// instead of exposing individual star icons. -class AccessibleRatingBar extends StatelessWidget { - const AccessibleRatingBar({ - required this.rating, - required this.maxRating, - super.key, - }); - - final int rating; - final int maxRating; - +class const AccessibleRatingBar({ + required final int rating, + required final int maxRating, + super.key, +}) extends StatelessWidget { @override Widget build(BuildContext context) { return Semantics( @@ -53,18 +48,12 @@ Cupertino widgets ship with weaker semantic defaults than their Material equival import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; -class AccessibleCupertinoSwitch extends StatelessWidget { - const AccessibleCupertinoSwitch({ - required this.label, - required this.value, - required this.onChanged, - super.key, - }); - - final String label; - final bool value; - final ValueChanged onChanged; - +class const AccessibleCupertinoSwitch({ + required final String label, + required final bool value, + required final ValueChanged onChanged, + super.key, +}) extends StatelessWidget { @override Widget build(BuildContext context) { return Semantics( @@ -78,22 +67,14 @@ class AccessibleCupertinoSwitch extends StatelessWidget { } } -class AccessibleCupertinoSlider extends StatelessWidget { - const AccessibleCupertinoSlider({ - required this.label, - required this.value, - required this.min, - required this.max, - required this.onChanged, - super.key, - }); - - final String label; - final double value; - final double min; - final double max; - final ValueChanged onChanged; - +class const AccessibleCupertinoSlider({ + required final String label, + required final double value, + required final double min, + required final double max, + required final ValueChanged onChanged, + super.key, +}) extends StatelessWidget { @override Widget build(BuildContext context) { return Semantics( @@ -173,11 +154,8 @@ import 'package:flutter/material.dart'; enum UploadStatus { idle, uploading, success, error } -class UploadStatusIndicator extends StatelessWidget { - const UploadStatusIndicator({required this.status, super.key}); - - final UploadStatus status; - +class const UploadStatusIndicator({required final UploadStatus status, super.key}) + extends StatelessWidget { @override Widget build(BuildContext context) { final icon = switch (status) { @@ -226,18 +204,12 @@ import 'package:flutter/material.dart'; /// Wraps any small widget in a minimum 48x48 touch target. /// 48 is the VGV recommended minimum. The WCAG 2.2 AA floor (2.5.8) is 24. -class AccessibleTapTarget extends StatelessWidget { - const AccessibleTapTarget({ - required this.onTap, - required this.semanticLabel, - required this.child, - super.key, - }); - - final VoidCallback onTap; - final String semanticLabel; - final Widget child; - +class const AccessibleTapTarget({ + required final VoidCallback onTap, + required final String semanticLabel, + required final Widget child, + super.key, +}) extends StatelessWidget { @override Widget build(BuildContext context) { return Semantics( @@ -265,16 +237,11 @@ AccessibleTapTarget( ### Dismissible With a Non-Drag Alternative (WCAG 2.2 2.5.7) ```dart -class AccessibleDismissibleListItem extends StatelessWidget { - const AccessibleDismissibleListItem({ - required this.item, - required this.onDelete, - super.key, - }); - - final Item item; - final ValueChanged onDelete; - +class const AccessibleDismissibleListItem({ + required final Item item, + required final ValueChanged onDelete, + super.key, +}) extends StatelessWidget { @override Widget build(BuildContext context) { return Dismissible( @@ -298,16 +265,11 @@ class AccessibleDismissibleListItem extends StatelessWidget { ### Reorderable List With Up/Down Buttons (WCAG 2.2 2.5.7) ```dart -class AccessibleReorderableList extends StatelessWidget { - const AccessibleReorderableList({ - required this.items, - required this.onReorder, - super.key, - }); - - final List items; - final void Function(int oldIndex, int newIndex) onReorder; - +class const AccessibleReorderableList({ + required final List items, + required final void Function(int oldIndex, int newIndex) onReorder, + super.key, +}) extends StatelessWidget { @override Widget build(BuildContext context) { return ReorderableListView.builder( @@ -349,24 +311,15 @@ class AccessibleReorderableList extends StatelessWidget { Material's `Slider` already responds to arrow keys. Custom slider widgets must do the same. ```dart -class AccessibleSlider extends StatelessWidget { - const AccessibleSlider({ - required this.label, - required this.value, - required this.onChanged, - this.min = 0, - this.max = 100, - this.step = 1, - super.key, - }); - - final String label; - final double value; - final double min; - final double max; - final double step; - final ValueChanged onChanged; - +class const AccessibleSlider({ + required final String label, + required final double value, + required final ValueChanged onChanged, + final double min = 0, + final double max = 100, + final double step = 1, + super.key, +}) extends StatelessWidget { @override Widget build(BuildContext context) { return Row( @@ -450,16 +403,11 @@ class AccessibleForm extends StatelessWidget { ### Focus Not Obscured: ensureVisible on Sticky-Header Layouts (WCAG 2.2 2.4.11) ```dart -class FocusEnsureVisibleField extends StatefulWidget { - const FocusEnsureVisibleField({ - required this.label, - required this.stickyHeaderHeight, - super.key, - }); - - final String label; - final double stickyHeaderHeight; - +class const FocusEnsureVisibleField({ + required final String label, + required final double stickyHeaderHeight, + super.key, +}) extends StatefulWidget { @override State createState() => _FocusEnsureVisibleFieldState(); } @@ -507,16 +455,11 @@ class _FocusEnsureVisibleFieldState extends State { ### Focus Appearance at AAA (WCAG 2.2 2.4.13) ```dart -class FocusAppearanceWrap extends StatefulWidget { - const FocusAppearanceWrap({ - required this.onPressed, - required this.child, - super.key, - }); - - final VoidCallback onPressed; - final Widget child; - +class const FocusAppearanceWrap({ + required final VoidCallback onPressed, + required final Widget child, + super.key, +}) extends StatefulWidget { @override State createState() => _FocusAppearanceWrapState(); } @@ -592,10 +535,8 @@ ThemeData buildAccessibleTheme() { ### Status Indicators Without Color Dependency ```dart -class AccessibleStatusBadge extends StatelessWidget { - const AccessibleStatusBadge({required this.status, super.key}); - final TaskStatus status; - +class const AccessibleStatusBadge({required final TaskStatus status, super.key}) + extends StatelessWidget { @override Widget build(BuildContext context) { final (icon, label, color) = switch (status) { @@ -627,16 +568,11 @@ enum TaskStatus { pending, active, complete, error } ### Adaptive Card Layout (Holds at 2x Android, 3x iOS) ```dart -class AdaptiveInfoCard extends StatelessWidget { - const AdaptiveInfoCard({ - required this.title, - required this.description, - super.key, - }); - - final String title; - final String description; - +class const AdaptiveInfoCard({ + required final String title, + required final String description, + super.key, +}) extends StatelessWidget { @override Widget build(BuildContext context) { return Card( @@ -694,12 +630,8 @@ class AccessiblePageRoute extends MaterialPageRoute { ### Hero Animation With Reduced-Motion Support ```dart -class AccessibleHero extends StatelessWidget { - const AccessibleHero({required this.tag, required this.child, super.key}); - - final Object tag; - final Widget child; - +class const AccessibleHero({required final Object tag, required final Widget child, super.key}) + extends StatelessWidget { @override Widget build(BuildContext context) { if (MediaQuery.of(context).disableAnimations) { @@ -741,10 +673,8 @@ class CheckoutEmailField extends StatelessWidget { ### Accessible Authentication: Allow Paste, Support Password Managers (WCAG 2.2 3.3.8) ```dart -class AccessiblePasswordField extends StatelessWidget { - const AccessiblePasswordField({required this.controller, super.key}); - final TextEditingController controller; - +class const AccessiblePasswordField({required final TextEditingController controller, super.key}) + extends StatelessWidget { @override Widget build(BuildContext context) { return TextField( diff --git a/skills/animations/SKILL.md b/skills/animations/SKILL.md index 3adf83a..4bfc5fe 100644 --- a/skills/animations/SKILL.md +++ b/skills/animations/SKILL.md @@ -30,6 +30,7 @@ Apply these standards to ALL animation work: - **Use `SingleTickerProviderStateMixin` for one controller** — use `TickerProviderStateMixin` only when the widget owns multiple controllers - **Keep animated subtrees small** — wrap only the widgets that change inside the animation builder, not entire widget trees - **Never animate a layout-triggering property** — `width`, `height`, `padding` and `SizedBox` dimensions force a fresh layout pass on every frame, in a one-child tree as much as in a deep one. Animate a `Transform` instead, `Transform.scale` for size and `Transform.translate` for position, or `Opacity` for fade, since those run on the compositing layer and skip layout +- **Dart 3.13 primary constructors** — on the Dart 3.13+ baseline (`very_good_core` 1.6 / `very_good_analysis` 11), declare widget fields as primary-constructor declaring parameters (`class const LabelChip({required final String label, super.key}) extends StatelessWidget`) rather than `this.field`; keep the classic form only below 3.13 --- @@ -118,11 +119,8 @@ Use implicit animations when the widget rebuilds with new target values. The fra Compose one `AnimatedFoo` per property when several move together. This is the entry-animation shape — a widget hidden until its data arrives, then fading in and sliding into place: ```dart -class SummaryCard extends StatelessWidget { - const SummaryCard({required this.summary, super.key}); - - final Summary? summary; - +class const SummaryCard({required final Summary? summary, super.key}) + extends StatelessWidget { @override Widget build(BuildContext context) { final hasData = summary != null; diff --git a/skills/animations/references/explicit-animations.md b/skills/animations/references/explicit-animations.md index 31ca8e4..37fdf90 100644 --- a/skills/animations/references/explicit-animations.md +++ b/skills/animations/references/explicit-animations.md @@ -127,18 +127,11 @@ Do not start animations in `build()`. Use `initState` for initial playback and ` Expose an optional controller parameter to allow tests to drive the animation directly: ```dart -class PulsingDot extends StatefulWidget { - const PulsingDot({ - required this.isActive, - super.key, - @visibleForTesting this.controller, - }); - - final bool isActive; - - @visibleForTesting - final AnimationController? controller; - +class const PulsingDot({ + required final bool isActive, + super.key, + @visibleForTesting final AnimationController? controller, +}) extends StatefulWidget { @override State createState() => _PulsingDotState(); } diff --git a/skills/animations/references/implicit-animations.md b/skills/animations/references/implicit-animations.md index 933bade..6adef55 100644 --- a/skills/animations/references/implicit-animations.md +++ b/skills/animations/references/implicit-animations.md @@ -17,11 +17,8 @@ entry-animation shape — a widget hidden until its data arrives, then fading in into place: ```dart -class SummaryCard extends StatelessWidget { - const SummaryCard({required this.summary, super.key}); - - final Summary? summary; - +class const SummaryCard({required final Summary? summary, super.key}) + extends StatelessWidget { @override Widget build(BuildContext context) { final hasData = summary != null; diff --git a/skills/animations/references/page-transitions.md b/skills/animations/references/page-transitions.md index 2b1a86c..5b6e52e 100644 --- a/skills/animations/references/page-transitions.md +++ b/skills/animations/references/page-transitions.md @@ -93,11 +93,7 @@ abstract class AppPageTransitions { path: 'details/:id', ) @immutable -class DetailsPageRoute extends GoRouteData { - const DetailsPageRoute({required this.id}); - - final String id; - +class const DetailsPageRoute({required final String id}) extends GoRouteData { @override Page buildPage(BuildContext context, GoRouterState state) { return AppPageTransitions.slideFade( diff --git a/skills/animations/references/staggered-animations.md b/skills/animations/references/staggered-animations.md index a1ad8a8..9bf9b64 100644 --- a/skills/animations/references/staggered-animations.md +++ b/skills/animations/references/staggered-animations.md @@ -78,20 +78,13 @@ class _StaggeredEntryState extends State Animate list items sequentially by offsetting each item's delay: ```dart -class StaggeredListItem extends StatefulWidget { - const StaggeredListItem({ - required this.index, - required this.itemCount, - required this.animation, - required this.child, - super.key, - }); - - final int index; - final int itemCount; - final Animation animation; - final Widget child; - +class const StaggeredListItem({ + required final int index, + required final int itemCount, + required final Animation animation, + required final Widget child, + super.key, +}) extends StatefulWidget { @override State createState() => _StaggeredListItemState(); } diff --git a/skills/bloc/SKILL.md b/skills/bloc/SKILL.md index ff45cb3..8833249 100644 --- a/skills/bloc/SKILL.md +++ b/skills/bloc/SKILL.md @@ -35,6 +35,7 @@ Apply these standards to ALL Bloc/Cubit work: - **Business logic in Bloc/Cubit only** — never in widgets, pages, or views - **Single responsibility** — one Bloc/Cubit per feature concern - **Emit only after async checks** — use `emit` only inside the handler callback +- **Dart 3.13 primary constructors** — on the Dart 3.13+ baseline (`very_good_core` 1.6 / `very_good_analysis` 11), declare event, state, and widget fields as primary-constructor declaring parameters (`class const ProfileRequested(final String userId) extends ProfileEvent`) rather than `this.field`; keep the classic form only below 3.13 --- @@ -106,11 +107,8 @@ sealed class TodoListEvent extends Equatable { final class TodoListSubscriptionRequested extends TodoListEvent {} -final class TodoListTodoDeleted extends TodoListEvent { - const TodoListTodoDeleted({required this.todo}); - - final Todo todo; - +final class const TodoListTodoDeleted({required final Todo todo}) + extends TodoListEvent { @override List get props => [todo]; } @@ -139,19 +137,12 @@ sealed class LoginState extends Equatable { final class LoginInitial extends LoginState {} final class LoginInProgress extends LoginState {} -final class LoginSuccess extends LoginState { - const LoginSuccess({required this.user}); - - final User user; - +final class const LoginSuccess({required final User user}) extends LoginState { @override List get props => [user]; } -final class LoginFailure extends LoginState { - const LoginFailure({required this.error}); - - final String error; - +final class const LoginFailure({required final String error}) + extends LoginState { @override List get props => [error]; } diff --git a/skills/bloc/references/patterns.md b/skills/bloc/references/patterns.md index bfa010b..dbb7821 100644 --- a/skills/bloc/references/patterns.md +++ b/skills/bloc/references/patterns.md @@ -80,17 +80,11 @@ Use one state class with a status enum when every state shares the same data sha ```dart enum TodoListStatus { initial, loading, success, failure } -class TodoListState extends Equatable { - const TodoListState({ - this.status = TodoListStatus.initial, - this.todos = const [], - this.error, - }); - - final TodoListStatus status; - final List todos; - final String? error; - +class const TodoListState({ + final TodoListStatus status = TodoListStatus.initial, + final List todos = const [], + final String? error, +}) extends Equatable { TodoListState copyWith({ TodoListStatus? status, List? todos, diff --git a/skills/internationalization/SKILL.md b/skills/internationalization/SKILL.md index 401d166..0a61522 100644 --- a/skills/internationalization/SKILL.md +++ b/skills/internationalization/SKILL.md @@ -33,6 +33,7 @@ Apply these standards to ALL internationalization work: - **Use `EdgeInsetsDirectional` (start/end) instead of `EdgeInsets` (left/right)** — ensures correct layout in RTL languages - **Handle RTL layout properly** — use directional widgets for padding, positioning, and alignment - **Implement i18n early** — even if only one language is planned initially, the overhead is small and the long-term benefit is significant +- **Dart 3.13 primary constructors** — on the Dart 3.13+ baseline (`very_good_core` 1.6 / `very_good_analysis` 11), declare a reusable widget's localized-string fields as primary-constructor declaring parameters (`class const ConfirmDialog({required final String title, super.key}) extends StatelessWidget`) rather than `this.field`; keep the classic form only below 3.13 ## Setup Pipeline and ARB File Format @@ -66,20 +67,13 @@ When someone asks to add `AppLocalizations` to a shared package, decline and say ```dart // Shared widget — no l10n dependency -class ConfirmDialog extends StatelessWidget { - const ConfirmDialog({ - required this.title, - required this.message, - required this.confirmLabel, - required this.cancelLabel, - super.key, - }); - - final String title; - final String message; - final String confirmLabel; - final String cancelLabel; - +class const ConfirmDialog({ + required final String title, + required final String message, + required final String confirmLabel, + required final String cancelLabel, + super.key, +}) extends StatelessWidget { @override Widget build(BuildContext context) { return AlertDialog( diff --git a/skills/layered-architecture/SKILL.md b/skills/layered-architecture/SKILL.md index 3b4296c..3094e74 100644 --- a/skills/layered-architecture/SKILL.md +++ b/skills/layered-architecture/SKILL.md @@ -38,6 +38,7 @@ Apply these standards to ALL layered architecture work: - **Barrel exports at every package boundary** — `src/` is never imported directly by consumers - **Repositories accept data layer dependencies via constructor injection** — never instantiate clients internally - **App bootstrap wires all layers** — `main_.dart` creates clients and repositories, provides them via `RepositoryProvider` +- **Dart 3.13 primary constructors** — on the Dart 3.13+ baseline (`very_good_core` 1.6 / `very_good_analysis` 11), declare model and widget fields as primary-constructor declaring parameters (`class const User(final String id, final String name) extends Equatable`) rather than `this.field`; keep the classic form only below 3.13 > **Cross-harness fallback.** This skill scaffolds and tests packages via the Very Good CLI MCP server. On a host without this plugin's Bash hooks and without that MCP server connected, run the equivalent `very_good create dart_package …`, `very_good packages get`, and `very_good test` commands directly. @@ -187,19 +188,12 @@ Domain models extend `Equatable` and represent the app's internal data shape — ```dart /// Domain model — lives in the repository package, NOT the data package. /// Fields match the app's needs, not the API schema. -class User extends Equatable { - const User({ - required this.id, - required this.email, - required this.displayName, - this.avatarUrl, - }); - - final String id; - final String email; - final String displayName; - final String? avatarUrl; - +class const User({ + required final String id, + required final String email, + required final String displayName, + final String? avatarUrl, +}) extends Equatable { @override List get props => [id, email, displayName, avatarUrl]; } diff --git a/skills/layered-architecture/references/model-transformation.md b/skills/layered-architecture/references/model-transformation.md index 2e1390c..f30b6cb 100644 --- a/skills/layered-architecture/references/model-transformation.md +++ b/skills/layered-architecture/references/model-transformation.md @@ -16,14 +16,12 @@ Add a named factory on the domain model to transform from the response model: import 'package:equatable/equatable.dart'; import 'package:user_api_client/user_api_client.dart' show UserResponse; -class User extends Equatable { - const User({ - required this.id, - required this.email, - required this.displayName, - this.avatarUrl, - }); - +class const User({ + required final String id, + required final String email, + required final String displayName, + final String? avatarUrl, +}) extends Equatable { /// Creates a [User] from a [UserResponse]. factory User.fromResponse(UserResponse response) { return User( @@ -34,11 +32,6 @@ class User extends Equatable { ); } - final String id; - final String email; - final String displayName; - final String? avatarUrl; - @override List get props => [id, email, displayName, avatarUrl]; } diff --git a/skills/layered-architecture/references/worked-example.md b/skills/layered-architecture/references/worked-example.md index 1e56569..ecfc6ee 100644 --- a/skills/layered-architecture/references/worked-example.md +++ b/skills/layered-architecture/references/worked-example.md @@ -51,24 +51,15 @@ import 'package:json_annotation/json_annotation.dart'; part 'user_response.g.dart'; @JsonSerializable() -class UserResponse { - const UserResponse({ - required this.id, - required this.email, - required this.displayName, - this.avatarUrl, - }); - +class const UserResponse({ + required final String id, + required final String email, + @JsonKey(name: 'display_name') required final String displayName, + @JsonKey(name: 'avatar_url') final String? avatarUrl, +}) { factory UserResponse.fromJson(Map json) => _$UserResponseFromJson(json); - final String id; - final String email; - @JsonKey(name: 'display_name') - final String displayName; - @JsonKey(name: 'avatar_url') - final String? avatarUrl; - Map toJson() => _$UserResponseToJson(this); } ``` @@ -82,12 +73,8 @@ import 'package:http/http.dart' as http; import 'package:user_api_client/user_api_client.dart'; /// Exception thrown when a user API request fails. -class UserApiException implements Exception { - const UserApiException(this.statusCode, this.message); - - final int statusCode; - final String message; -} +class const UserApiException(final int statusCode, final String message) + implements Exception; /// HTTP client for the User API. class UserApiClient { @@ -224,19 +211,12 @@ export 'user.dart'; import 'package:equatable/equatable.dart'; /// Domain model representing a user. -class User extends Equatable { - const User({ - required this.id, - required this.email, - required this.displayName, - this.avatarUrl, - }); - - final String id; - final String email; - final String displayName; - final String? avatarUrl; - +class const User({ + required final String id, + required final String email, + required final String displayName, + final String? avatarUrl, +}) extends Equatable { @override List get props => [id, email, displayName, avatarUrl]; } @@ -249,11 +229,7 @@ import 'package:user_api_client/user_api_client.dart'; import 'package:user_repository/user_repository.dart'; /// Exception thrown when a user is not found. -class UserNotFoundException implements Exception { - const UserNotFoundException(this.userId); - - final String userId; -} +class const UserNotFoundException(final String userId) implements Exception; /// Repository for user data. /// @@ -302,11 +278,8 @@ sealed class ProfileEvent extends Equatable { List get props => []; } -final class ProfileLoadRequested extends ProfileEvent { - const ProfileLoadRequested({required this.userId}); - - final String userId; - +final class const ProfileLoadRequested({required final String userId}) + extends ProfileEvent { @override List get props => [userId]; } @@ -332,11 +305,8 @@ final class ProfileLoading extends ProfileState { const ProfileLoading(); } -final class ProfileSuccess extends ProfileState { - const ProfileSuccess({required this.user}); - - final User user; - +final class const ProfileSuccess({required final User user}) + extends ProfileState { @override List get props => [user]; } @@ -398,11 +368,8 @@ import 'package:my_app/profile/bloc/profile_bloc.dart'; import 'package:my_app/profile/view/profile_view.dart'; import 'package:user_repository/user_repository.dart'; -class ProfilePage extends StatelessWidget { - const ProfilePage({required this.userId, super.key}); - - final String userId; - +class const ProfilePage({required final String userId, super.key}) + extends StatelessWidget { @override Widget build(BuildContext context) { return BlocProvider( @@ -495,14 +462,10 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:user_repository/user_repository.dart'; -class App extends StatelessWidget { - const App({ - required this.userRepository, - super.key, - }); - - final UserRepository userRepository; - +class const App({ + required final UserRepository userRepository, + super.key, +}) extends StatelessWidget { @override Widget build(BuildContext context) { return RepositoryProvider.value( diff --git a/skills/navigation/references/parameters.md b/skills/navigation/references/parameters.md index a5b8606..ad654b2 100644 --- a/skills/navigation/references/parameters.md +++ b/skills/navigation/references/parameters.md @@ -10,11 +10,8 @@ Use path parameters to identify specific resources: path: 'article/:id', ) @immutable -class FlutterArticlePageRoute extends GoRouteData { - const FlutterArticlePageRoute({required this.id}); - - final String id; - +class const FlutterArticlePageRoute({required final String id}) + extends GoRouteData { @override Widget build(BuildContext context, GoRouterState state) { return FlutterArticlePage(id: id); @@ -34,15 +31,10 @@ Use query parameters for optional filtering or sorting criteria: path: 'articles', ) @immutable -class FlutterArticlesPageRoute extends GoRouteData { - const FlutterArticlesPageRoute({ - this.date, - this.category, - }); - - final String? date; - final String? category; - +class const FlutterArticlesPageRoute({ + final String? date, + final String? category, +}) extends GoRouteData { @override Widget build(BuildContext context, GoRouterState state) { return FlutterArticlesPage( diff --git a/skills/navigation/references/typed-routes.md b/skills/navigation/references/typed-routes.md index 130b4a7..0fa674c 100644 --- a/skills/navigation/references/typed-routes.md +++ b/skills/navigation/references/typed-routes.md @@ -12,15 +12,10 @@ casting. `package:go_router_builder` generates the type-safe helpers at build ti path: '/categories', ) @immutable -class CategoriesPageRoute extends GoRouteData { - const CategoriesPageRoute({ - this.size, - this.color, - }); - - final String? size; - final String? color; - +class const CategoriesPageRoute({ + final String? size, + final String? color, +}) extends GoRouteData { @override Widget build(BuildContext context, GoRouterState state) { return CategoriesPage(size: size, color: color); diff --git a/skills/very-good-analysis-upgrade/SKILL.md b/skills/very-good-analysis-upgrade/SKILL.md index f54a9b2..610d3e3 100644 --- a/skills/very-good-analysis-upgrade/SKILL.md +++ b/skills/very-good-analysis-upgrade/SKILL.md @@ -242,4 +242,4 @@ Naming only "there is a conflict" is not enough — name the two constraints. ## Additional Resources -See [`references/lint-fixes.md`](references/lint-fixes.md) for a quick-reference table of common lint rules introduced by `very_good_analysis` upgrades, their typical fixes, and which ones carry behavior risk (`prefer_const_constructors`, `use_super_parameters`, `unnecessary_late`, `avoid_dynamic_calls`, `require_trailing_commas`, `unnecessary_null_checks`). +See [`references/lint-fixes.md`](references/lint-fixes.md) for a quick-reference table of common lint rules introduced by `very_good_analysis` upgrades, their typical fixes, and which ones carry behavior risk (`prefer_const_constructors`, `use_super_parameters`, `unnecessary_late`, `avoid_dynamic_calls`, `require_trailing_commas`, `unnecessary_null_checks`) — plus the Dart 3.13 constructor rules that `very_good_analysis` 11.0.0 adds (`unnecessary_type_name_in_constructor`, `use_declaring_parameters`, `unnecessary_primary_constructor_body`, `empty_container_bodies`). diff --git a/skills/very-good-analysis-upgrade/references/lint-fixes.md b/skills/very-good-analysis-upgrade/references/lint-fixes.md index d1dd9d6..6b42822 100644 --- a/skills/very-good-analysis-upgrade/references/lint-fixes.md +++ b/skills/very-good-analysis-upgrade/references/lint-fixes.md @@ -20,3 +20,69 @@ silently: name the risk and hand the decision to a human, per the "Avoid behavio core standard. `avoid_dynamic_calls` is the usual one — `map['total'] as double` throws a `TypeError` on a JSON integer that `map['total'].toStringAsFixed(2)` handled, so the cast is a code change wearing a lint's clothes and belongs in its own reviewed PR. + +--- + +## very_good_analysis 11.0.0 — Dart 3.13 constructor lints + +`very_good_analysis` 11.0.0 follows `very_good_core` 1.6.0 onto Dart 3.13 and enables four new +constructor rules. All four are style-only (no behavior risk), but each only has a valid fix once +the package targets Dart 3.13 or newer — below that SDK, leave the classic form and raise any SDK +bump as its own change. + +- `unnecessary_type_name_in_constructor` — a class-body constructor that repeats the class name + (`const ProfileCard(...)`) should name the unnamed constructor `new` instead. **This is the rule + that flags most existing widget, model, event, and state code**, and its minimal fix is one word: + rename `ProfileCard(...)` to `new(...)`. It does not force a primary constructor on anyone. +- `use_declaring_parameters` — inside a **primary constructor**, a parameter written as an + initializing formal (`this.field`) should instead be a declaring parameter (`final Type field`). + It only fires once the class already uses a primary-constructor header, so it never touches a + classic class-body constructor. +- `unnecessary_primary_constructor_body` — drop an empty `{}` body on a primary constructor and end + the header with `;`. +- `empty_container_bodies` — replace an empty `{}` body (class, mixin, or extension type) with `;`. + +The minimal fix for `unnecessary_type_name_in_constructor` leaves the class shape untouched — it +only renames the unnamed constructor to `new`: + +```dart +// Before — flagged by unnecessary_type_name_in_constructor on Dart 3.13+ +class ProfileCard extends StatelessWidget { + const ProfileCard({required this.userId, super.key}); + + final String userId; + + @override + Widget build(BuildContext context) => Text(userId); +} + +// Minimal fix — name the unnamed constructor `new` +class ProfileCard extends StatelessWidget { + const new({required this.userId, super.key}); + + final String userId; + + @override + Widget build(BuildContext context) => Text(userId); +} +``` + +To modernize further, promote the fields to primary-constructor **declaring parameters**. `const` +sits between `class` and the class name, and the inheritance clause follows the parameter list. +This form clears both rules at once and is what the widget, model, event, and state skills show on +the Dart 3.13 baseline: + +```dart +// Modernized — a primary constructor with a declaring parameter +class const ProfileCard({required final String userId, super.key}) + extends StatelessWidget { + @override + Widget build(BuildContext context) => Text(userId); +} +``` + +`unnecessary_type_name_in_constructor` can touch a large share of a codebase — every unnamed +class-body constructor is a candidate — but it is a new warning the bump introduced, so it belongs +in the upgrade PR. Promoting to declaring parameters is a further modernization, not a forced fix; +`new` alone satisfies the lint. Apply either form only when the package already targets Dart 3.13; +if the bump also forces an SDK-constraint change, that belongs in its own PR.