Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
03e7e0d
Add the `bloc_related_class_naming` lint
PiotrRogulski May 9, 2025
e618f17
Add tests for `bloc_related_class_naming`
PiotrRogulski May 9, 2025
c7faf0b
Avoid conflict of event classes
PiotrRogulski May 9, 2025
b5d3e85
Add the `prefer_equatable_mixin` lint
PiotrRogulski May 9, 2025
cd0e922
Add tests for `prefer_equatable_mixin`
PiotrRogulski May 9, 2025
78b6a0d
Add the `bloc_subclasses_naming` lint
PiotrRogulski May 9, 2025
911b7a7
Add tests for `bloc_subclasses_naming`
PiotrRogulski May 9, 2025
4094c28
Add the `bloc_class_modifiers` lint
PiotrRogulski May 9, 2025
0fba727
Add tests for `bloc_class_modifiers`
PiotrRogulski May 9, 2025
b816990
Add the `bloc_related_classes_equatable` lint
PiotrRogulski May 9, 2025
8af91e0
Add tests for `bloc_related_classes_equatable`
PiotrRogulski May 9, 2025
2ec49e5
Add the `bloc_const_constructors` lint
PiotrRogulski May 9, 2025
68be7ef
Add tests for `bloc_const_constructors`
PiotrRogulski May 9, 2025
41034b3
Collect type checkers instances
PiotrRogulski May 9, 2025
b3e78b2
Clean up tests
PiotrRogulski May 9, 2025
577c203
Add new lints to README.md
PiotrRogulski May 9, 2025
99c86fe
Simplify checking types
PiotrRogulski May 9, 2025
9156ed7
Merge remote-tracking branch 'origin/master' into feature/new-bloc-lints
PiotrRogulski Aug 13, 2025
27f12a2
Replace deprecated usages
PiotrRogulski Aug 13, 2025
c627c59
Add class modifier test for classes without subclasses
PiotrRogulski Aug 13, 2025
3db10b9
Make fix classes private
PiotrRogulski Aug 13, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
197 changes: 197 additions & 0 deletions packages/leancode_lint/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -413,6 +413,203 @@ class MyWidget extends StatelessWidget {

None

### `bloc_class_modifiers`

**DO** add `final` or `sealed` modifiers to bloc state, event, and presentation event classes.

**BAD:**

```dart
class MyState {}

class MyStateInitial extends MyState {}

class MyStateLoading extends MyState {}
```

**GOOD:**

```dart
sealed class MyState {}

final class MyStateInitial extends MyState {}

final class MyStateLoading extends MyState {}
```

#### Configuration

None.

### `bloc_const_constructors`

**DO** define unnamed const constructors for bloc state, event, and presentation event classes.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ditto, it would be nice to highlight that this lint works only if bloc is in the same file as states, events etc


**BAD:**

```dart
class MyState {}
```

**BAD:**

```dart
class MyState {
MyState();
}
```

**GOOD:**

```dart
class MyState {
const MyState();
}
```

#### Configuration

None.

### `bloc_related_classes_equatable`

**DO** mix in `EquatableMixin` in bloc state, event, and presentation event classes.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is obvious why it is better to use this mixin, but we could add some info about it here.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reasoning from the issue can be mentioned or quoted: felangel/equatable#160


**BAD:**

```dart
class MyState {
MyState(this.value);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
MyState(this.value);
const MyState(this.value);


final int value;
}
```

**GOOD:**

```dart
import 'package:equatable/equatable.dart';

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

final int value;

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

#### Configuration

None.

### `bloc_related_class_naming`

**DO** prefix bloc state, event, and presentation event classes with the name of the bloc.

**BAD:**

```dart
class MyAwesomeBloc extends Bloc<WrongEvent, SomeState> {
// ...
}

class WrongEvent {}

class SomeState {}
```

**GOOD:**

```dart
class MyAwesomeBloc extends Bloc<MyAwesomeEvent, MyAwesomeState> {
// ...
}

class MyAwesomeEvent {}

class MyAwesomeState {}
```

#### Configuration

None.

### `bloc_subclasses_naming`

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

**BAD:**

```dart
class MyAwesomeBloc extends Bloc<MyAwesomeEvent, MyAwesomeState> {
// ...
}

sealed class MyAwesomeState {}

final class SomeState extends MyAwesomeState {}

final class AnotherState extends MyAwesomeState {}
```

**GOOD:**

```dart
class MyAwesomeBloc extends Bloc<MyAwesomeEvent, MyAwesomeState> {
// ...
}

sealed class MyAwesomeState {}

final class MyAwesomeStateInitial extends MyAwesomeState {}

final class MyAwesomeStateLoading extends MyAwesomeState {}
```

#### Configuration

None.

### `prefer_equatable_mixin`

**DO** mix in `EquatableMixin` instead of extending `Equatable`.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So disappointing that a lint is needed for that. Equatable should have been binned ages ago


**BAD:**

```dart
import 'package:equatable/equatable.dart';

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

final int value;

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

**GOOD:**

```dart
import 'package:equatable/equatable.dart';

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

final int value;

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

#### Configuration

None.

## 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 <kbd>ctrl</kbd>+<kbd>.</kbd> or <kbd>⌘</kbd>+<kbd>.</kbd>.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/source/source_range.dart';
import 'package:custom_lint_builder/custom_lint_builder.dart';
import 'package:leancode_lint/common_type_checkers.dart';
import 'package:leancode_lint/helpers.dart';

/// Converts an iterable call to [Iterable.map] with an optional
Expand Down Expand Up @@ -39,8 +40,6 @@ class ConvertIterableMapToCollectionFor extends DartAssist {
}

void _handleIterable(MethodInvocation node, ChangeReporter reporter) {
const iterableChecker = TypeChecker.fromUrl('dart:core#Iterable');

if (node case MethodInvocation(
target: Expression(
staticType: final targetType?,
Expand All @@ -57,7 +56,7 @@ class ConvertIterableMapToCollectionFor extends DartAssist {
),
],
),
) when iterableChecker.isAssignableFromType(targetType)) {
) when TypeCheckers.iterable.isAssignableFromType(targetType)) {
final expression = maybeGetSingleReturnExpression(functionBody);
if (expression == null) {
return;
Expand Down
75 changes: 75 additions & 0 deletions packages/leancode_lint/lib/common_type_checkers.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import 'package:custom_lint_builder/custom_lint_builder.dart';

extension TypeCheckers on Never {
// dart
static const iterable = TypeChecker.fromUrl('dart:core#Iterable');

// equatable
static const equatable = TypeChecker.fromName(
'Equatable',
packageName: 'equatable',
);
static const equatableMixin = TypeChecker.fromName(
'EquatableMixin',
packageName: 'equatable',
);

// flutter
static const statelessWidget = TypeChecker.fromName(
'StatelessWidget',
packageName: 'flutter',
);
static const state = TypeChecker.fromName('State', packageName: 'flutter');
static const column = TypeChecker.fromName('Column', packageName: 'flutter');
static const row = TypeChecker.fromName('Row', packageName: 'flutter');
static const wrap = TypeChecker.fromName('Wrap', packageName: 'flutter');
static const flex = TypeChecker.fromName('Flex', packageName: 'flutter');
static const sliverList = TypeChecker.fromName(
'SliverList',
packageName: 'flutter',
);
static const sliverMainAxisGroup = TypeChecker.fromName(
'SliverMainAxisGroup',
packageName: 'flutter',
);
static const sliverCrossAxisGroup = TypeChecker.fromName(
'SliverCrossAxisGroup',
packageName: 'flutter',
);
static const sliverChildListDelegate = TypeChecker.fromName(
'SliverChildListDelegate',
packageName: 'flutter',
);

static const multiSliver = TypeChecker.fromName(
'MultiSliver',
packageName: 'sliver_tools',
);

// hooks
static const hookWidget = TypeChecker.fromName(
'HookWidget',
packageName: 'flutter_hooks',
);
static const hookBuilder = TypeChecker.fromName(
'HookBuilder',
packageName: 'flutter_hooks',
);
static const hookConsumer = TypeChecker.fromName(
'HookConsumer',
packageName: 'hooks_riverpod',
);
static const hookConsumerWidget = TypeChecker.fromName(
'HookConsumerWidget',
packageName: 'hooks_riverpod',
);

// bloc
static const cubit = TypeChecker.fromName('Cubit', packageName: 'bloc');
static const bloc = TypeChecker.fromName('Bloc', packageName: 'bloc');
static const blocBase = TypeChecker.fromName('BlocBase', packageName: 'bloc');
static const blocPresentation = TypeChecker.fromName(
'BlocPresentationMixin',
packageName: 'bloc_presentation',
);
}
Loading