Banner

leancode_lint

leancode_lint pub.dev badge

An opinionated set of high-quality, robust, and up-to-date lint rules used at LeanCode.

Usage

There are two supported ways to use this package:

  1. As-is (default configuration) – enable the built-in leancode_lint analyzer plugin. This uses const LeanCodeLintConfig() defaults.
  2. With custom configuration – create your own analyzer plugin package that instantiates LeanCodeLintPlugin with your LeanCodeLintConfig, then enable your plugin instead (see “Configuration (custom plugin package)” below).

If you only want to toggle individual rules on/off, you can do that in analysis_options.yaml (see “Custom lint rules”).

Installation

  1. Add leancode_lint as a dev dependency in your project's pubspec.yaml.

    dart pub add leancode_lint --dev
    
  2. In your analysis_options.yaml add include: package:leancode_lint/analysis_options.yaml. You might want to exclude some files (e.g generated json serializable) from analysis.

  3. Enable the analyzer plugin in analysis_options.yaml.

  4. Run flutter pub get in your project main directory and restart the analysis server in your IDE. You're ready to go!

Example analysis_options.yaml:

include: package:leancode_lint/analysis_options.yaml

plugins:
  leancode_lint: ^20.0.0

analyzer:
  exclude:
    - '**/*.g.dart'

Tip

See the example/ directory for complete, runnable examples with both default and custom configuration.

Configuration (custom plugin package)

This section is optional. You only need it if you want to customize the behavior of configurable rules beyond the built-in defaults.

To configure leancode_lint rules programmatically, create your own analyzer plugin package (e.g. my_lints) that depends on leancode_lint and exposes a top-level plugin variable.

my_lints/lib/main.dart:

import 'package:leancode_lint/plugin.dart';

final plugin = LeanCodeLintPlugin(
  name: 'my_lints',
  config: LeanCodeLintConfig(
    applicationPrefix: 'Lncd',
    catchParameterNames: CatchParameterNamesConfig(
      exception: 'error',
      stackTrace: 'stackTrace',
    ),
    designSystemItemReplacements: {
      'AppText': [
        DesignSystemForbiddenItem(name: 'Text', packageName: 'flutter'),
        DesignSystemForbiddenItem(name: 'RichText', packageName: 'flutter'),
      ],
      'AppScaffold': [
        DesignSystemForbiddenItem(name: 'Scaffold', packageName: 'flutter'),
      ],
    },
  ),
);

Then enable your plugin in the consuming project’s analysis_options.yaml:

include: package:leancode_lint/analysis_options.yaml

plugins:
  my_lints:
    path: ./path/to/my_lints

Usage in libraries

If your package is a library rather than a binary application, you will expose some public API for users of your library. Therefore, you should use lint rules optimized for this case by just changing your include entry in analysis_options.yaml:

include: package:leancode_lint/analysis_options_package.yaml

Custom lint rules

add_cubit_suffix_for_your_cubits

add_cubit_suffix_for_your_cubits

DO add a 'Cubit' suffix to your cubit names.

BAD:

class MyClass extends Cubit<int> {}

GOOD:

class MyClassCubit extends Cubit<int> {}

Configuration

None.

avoid_conditional_hooks

avoid_conditional_hooks

AVOID using hooks conditionally

BAD:

Widget build(BuildContext context) {
  if (condition) {
    useEffect(() {
      // ...
    }, []);
  }
}

BAD:

Widget build(BuildContext context) {
  final controller = this.controller ?? useTextEditingController();
}

BAD:

Widget build(BuildContext context) {
  if (condition) {
    return Text('Early return');
  }

  final state = useState(0);
}

GOOD:

Widget build(BuildContext context) {
  useEffect(() {
    if (condition) {
        // ...
    }
  }, []);
}

GOOD:

Widget build(BuildContext context) {
  final backingController = useTextEditingController();
  final controller = this.controller ?? backingController;
}

GOOD:

Widget build(BuildContext context) {
  final state = useState(0);

  if (condition) {
    return Text('Early return');
  }
}

Configuration

None.

bloc_related_class_naming

DO follow the naming convention for Bloc/Cubit related classes.

For ExampleBloc:

  • Event class: ExampleEvent
  • State class: ExampleState
  • Presentation Event class: ExamplePresentationEvent

For ExampleCubit:

  • State class: ExampleState
  • Presentation Event class: ExamplePresentationEvent

Note

This lint only checks classes defined in the same library (including parts) as the Bloc/Cubit. Presentation events are only checked if the bloc_presentation package is used.

BAD:

class MyBloc extends Bloc<WrongEvent, WrongState> {}

GOOD:

class MyBloc extends Bloc<MyEvent, MyState> {}

Configuration

Configured via LeanCodeLintConfig.blocRelatedClassNaming:

import 'package:leancode_lint/plugin.dart';

final plugin = LeanCodeLintPlugin(
  name: 'my_lints',
  config: LeanCodeLintConfig(
    blocRelatedClassNaming: BlocRelatedClassNamingConfig(
      stateSuffix: 'State',
      eventSuffix: 'Event',
      presentationEventSuffix: 'PresentationEvent',
    ),
  ),
);
bloc_subclasses_naming

bloc_subclasses_naming

DO prefix subclasses of bloc state, event, and presentation event classes with the name of the base class.

BAD:

class MyState {}
class Initial extends MyState {}
class Loaded extends MyState {}

class MyEvent {}
class Load extends MyEvent {}

class MyCubit extends Cubit<MyState> {}

GOOD:

class MyState {}
class MyStateInitial extends MyState {}
class MyStateLoaded extends MyState {}

class MyEvent {}
class MyEventLoad extends MyEvent {}

class MyCubit extends Cubit<MyState> {}

Note

This lint only triggers when a corresponding Bloc/Cubit exists in the same library. The required prefix is always the exact name of the base class, regardless of how it is named.

Configuration

None.

catch_parameter_names

catch_parameter_names

DO name catch clause parameters consistently

  • if it's a catch-all, the exception should be named err and the stacktrace st
  • if it's a typed catch, the stacktrace has to be named st

BAD:

void f() {
  try {} catch (e, s) {}
  try {} on SocketException catch (e, s) {}
}

GOOD:

void f() {
  try {} catch (err, st) {}
  try {} on SocketException catch (e, st) {}
}

GOOD:

With custom config: exception: error, stack_trace: stackTrace

void f() {
  try {} catch (error, stackTrace) {}
  try {} on SocketException catch (error, stackTrace) {}
}

Configuration

Configured via LeanCodeLintConfig.catchParameterNames:

import 'package:leancode_lint/plugin.dart';

final plugin = LeanCodeLintPlugin(
  name: 'my_lints',
  config: LeanCodeLintConfig(
    catchParameterNames: CatchParameterNamesConfig(
      exception: 'error',
      stackTrace: 'stackTrace',
    ),
  ),
);
hook_widget_does_not_use_hooks

hook_widget_does_not_use_hooks

AVOID extending HookWidget if no hooks are used.

BAD:

class MyWidget extends HookWidget {
  @override
  Widget build(BuildContext context) {
    return Placeholder();
  }
}

BAD:

class MyWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return HookBuilder(
      builder: (context) {
        return Placeholder();
      },
    );
  }
}

GOOD:

class MyWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Placeholder();
  }
}

GOOD:

Widget build(BuildContext context) {
  return Builder(
    builder: (context) {
      return Placeholder();
    },
  );
}

Configuration

None.

prefix_widgets_returning_slivers

prefix_widgets_returning_slivers

DO prefix widget names with 'Sliver' if the widget returns slivers.

BAD:

class MyWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return SliverToBoxAdapter();
  }
}

GOOD:

class SliverMyWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return SliverToBoxAdapter();
  }
}

Configuration

  • application_prefix: A string. Specifies the application prefix to accept sliver prefixes. For example if set to "Lncd" then "LncdSliverMyWidget" is a valid sliver name.

Configured via LeanCodeLintConfig.applicationPrefix:

import 'package:leancode_lint/plugin.dart';

final plugin = LeanCodeLintPlugin(
  name: 'my_lints',
  config: LeanCodeLintConfig(applicationPrefix: 'Lncd'),
);
start_comments_with_space

start_comments_with_space

DO start comments/docs with an empty space.

BAD:

//some comment
///some doc

GOOD:

// some comment
/// some doc

Configuration

None.

use_design_system_item

use_design_system_item

AVOID using items disallowed by the design system.

This rule has to be configured to do anything. The rule will highlight forbidden usages and suggest alternatives preferred by the design system.

Configuration

Configured via LeanCodeLintConfig.designSystemItemReplacements:

import 'package:leancode_lint/plugin.dart';

final plugin = LeanCodeLintPlugin(
  name: 'my_lints',
  config: LeanCodeLintConfig(
    designSystemItemReplacements: {
      'LncdText': [
        DesignSystemForbiddenItem(name: 'Text', packageName: 'flutter'),
        DesignSystemForbiddenItem(name: 'RichText', packageName: 'flutter'),
      ],
      'LncdScaffold': [
        DesignSystemForbiddenItem(name: 'Scaffold', packageName: 'flutter'),
      ],
    },
  ),
);

avoid_single_child_in_multi_child_widgets

avoid_single_child_in_multi_child_widgets

AVOID using Column, Row, Flex, Wrap, SliverList, MultiSliver, SliverChildListDelegate, SliverMainAxisGroup, and SliverCrossAxisGroup with a single child.

BAD:

Widget build(BuildContext context) {
  return Column(
    children: [
      Container(),
    ],
  );
}

GOOD:

Widget build(BuildContext context) {
  return Container();
}

Configuration

None.

use_dedicated_media_query_methods

use_dedicated_media_query_methods

AVOID using MediaQuery.of or MediaQuery.maybeOf to access only one property. Instead, prefer dedicated methods, for example MediaQuery.paddingOf(context).

Dedicated methods offer better performance by minimizing unnecessary widget rebuilds.

BAD:

Widget build(BuildContext context) {
  final size = MediaQuery.of(context).size;
  return const SizedBox();
}

GOOD:

Widget build(BuildContext context) {
  final size = MediaQuery.sizeOf(context);
  return const SizedBox();
}

Configuration

None.

use_align

use_align

DO Use the Align widget instead of the Container widget with only the alignment parameter.

BAD:

class MyWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container(
      alignment: Alignment.bottomCenter,
      child: const SizedBox(),
    );
  }
}

GOOD:

class MyWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return const Align(
      alignment: Alignment.bottomCenter,
      child: SizedBox(),
    );
  }
}

Configuration

None

use_padding

use_padding

DO Use Padding widget instead of the Container widget with only the margin parameter

BAD:

class MyWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container(
      margin: EdgeInsets.all(10),
      child: const SizedBox(),
    );
  }
}

GOOD:

class MyWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return const Padding(
      padding: EdgeInsets.all(10),
      child: SizedBox(),
    );
  }
}

Configuration

None

prefer_center_over_align

prefer_center_over_align

DO Use the Center widget instead of the Align widget with the alignment parameter set to Alignment.center

BAD:

class MyWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return const Align(
      child: SizedBox(),
    );
  }
}

GOOD:

class MyWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return const Center(
      child: SizedBox(),
    );
  }
}

Configuration

None

prefer_equatable_mixin

prefer_equatable_mixin

DO mix in EquatableMixin instead of extending Equatable.

BAD:

import 'package:equatable/equatable.dart';

class Foobar extends Equatable {
  const Foobar(this.value);

  final int value;

  @override
  List<Object?> get props => [value];
}

GOOD:

import 'package:equatable/equatable.dart';

class Foobar with EquatableMixin {
  const Foobar(this.value);

  final int value;

  @override
  List<Object?> get props => [value];
}

Configuration

None.

Disabling custom lint rules

To disable a particular custom lint rule, set the rule to false in analysis_options.yaml. For example, to disable prefix_widgets_returning_slivers:

plugins:
  leancode_lint:
    version: ^20.0.0
    diagnostics:
      prefix_widgets_returning_slivers: false

Assists

Assists are IDE refactorings not related to a particular issue. They can be triggered by placing your cursor over a relevant piece of code and opening the code actions dialog. For instance, in VSCode this is done with ctrl+. or +..

See linked source code containing explanation in dart doc.


🛠️ Maintained by LeanCode

LeanCode Logo

This package is built with 💙 by LeanCode. We are top-tier experts focused on Flutter Enterprise solutions.

Why LeanCode?

  • Creators of Patrol – the next-gen testing framework for Flutter.

  • Production-Ready – We use this package in apps with millions of users.

  • Full-Cycle Product Development – We take your product from scratch to long-term maintenance.


Need help with your Flutter project?

👉 Hire our team   •   Check our other packages

Libraries

config
main
plugin