Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 4 additions & 0 deletions lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import 'package:solid_lints/src/lints/avoid_unnecessary_type_assertions/fixes/av
import 'package:solid_lints/src/lints/avoid_unused_parameters/avoid_unused_parameters_rule.dart';
import 'package:solid_lints/src/lints/double_literal_format/double_literal_format_rule.dart';
import 'package:solid_lints/src/lints/double_literal_format/fixes/double_literal_format_fix.dart';
import 'package:solid_lints/src/lints/member_ordering/member_ordering_rule.dart';
import 'package:solid_lints/src/lints/proper_super_calls/proper_super_calls_rule.dart';

/// The entry point for the Solid Lints analyser server plugin.
Expand Down Expand Up @@ -49,6 +50,9 @@ class SolidLintsPlugin extends Plugin {
AvoidUnusedParametersRule(
analysisOptionsLoader: analysisLoader,
),
MemberOrderingRule(
analysisOptionsLoader: analysisLoader,
),
];

for (final lintRule in lintRules) {
Expand Down
1 change: 0 additions & 1 deletion lib/src/lints/member_ordering/config_parser.dart
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,6 @@ class MemberOrderingConfigParser {
'final_fields',
'init_state_method',
'var_fields',
'init_state_method',
'private_methods',
'overridden_public_methods',
'build_method',
Expand Down
172 changes: 60 additions & 112 deletions lib/src/lints/member_ordering/member_ordering_rule.dart
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import 'package:analyzer/error/listener.dart';
import 'package:custom_lint_builder/custom_lint_builder.dart';
import 'package:analyzer/analysis_rule/rule_context.dart';
import 'package:analyzer/analysis_rule/rule_visitor_registry.dart';
import 'package:analyzer/error/error.dart';
import 'package:solid_lints/src/lints/member_ordering/models/member_ordering_parameters.dart';
import 'package:solid_lints/src/lints/member_ordering/visitors/member_ordering_visitor.dart';
import 'package:solid_lints/src/models/rule_config.dart';
import 'package:solid_lints/src/models/solid_lint_rule.dart';
import 'package:solid_lints/src/models/solid_multi_lint_rule.dart';

/// A lint which allows to enforce a particular class member ordering
/// conventions.
Expand Down Expand Up @@ -45,14 +45,15 @@ import 'package:solid_lints/src/models/solid_lint_rule.dart';
/// Assuming config:
///
/// ```yaml
/// custom_lint:
/// rules:
/// - member_ordering:
/// alphabetize: true
/// order:
/// - fields
/// - getters_setters
/// - methods
/// plugins:
/// solid_lints:
/// diagnostics:
/// member_ordering:
/// alphabetize: true
/// order:
/// - fields
/// - getters_setters
/// - methods
/// ```
///
/// #### BAD:
Expand Down Expand Up @@ -82,113 +83,60 @@ import 'package:solid_lints/src/models/solid_lint_rule.dart';
/// void method() {}
/// }
/// ```
class MemberOrderingRule extends SolidLintRule<MemberOrderingParameters> {
/// This lint rule represents
/// the error whether we use bad formatted double literals.
class MemberOrderingRule extends SolidMultiLintRule<MemberOrderingParameters> {
/// The name of this lint rule.
static const lintName = 'member_ordering';

static const _warningMessage = 'should be before';
static const _warningAlphabeticalMessage = 'should be alphabetically before';
static const _warningTypeAlphabeticalMessage =
'type name should be alphabetically before';
/// Reported when a class member is declared out of order.
static const wrongOrderCode = LintCode(
lintName,
'{0} should be before {1}.',
uniqueName: 'wrong_order',
);

/// Reported when class members in the same group are not sorted
/// alphabetically.
static const alphabeticalOrderCode = LintCode(
lintName,
'{0} should be alphabetically before {1}.',
uniqueName: 'alphabetical_order',
);

/// Reported when class members in the same group are not sorted
/// alphabetically by type name.
static const alphabeticalByTypeOrderCode = LintCode(
lintName,
'{0} type name should be alphabetically before {1}.',
uniqueName: 'alphabetical_by_type_order',
);

/// Creates a new instance of [MemberOrderingRule].
MemberOrderingRule({
required super.analysisOptionsLoader,
}) : super(
name: lintName,
description: 'Enforces a particular class member ordering convention.',
parametersParser: MemberOrderingParameters.fromJson,
);

MemberOrderingRule._(super.config);

/// Creates a new instance of [MemberOrderingRule]
/// based on the lint configuration.
factory MemberOrderingRule.createRule(CustomLintConfigs configs) {
final config = RuleConfig<MemberOrderingParameters>(
configs: configs,
name: lintName,
paramsParser: MemberOrderingParameters.fromJson,
problemMessage: (_) => "Order of class member is wrong",
);

return MemberOrderingRule._(config);
}
@override
List<DiagnosticCode> get diagnosticCodes => [
wrongOrderCode,
alphabeticalOrderCode,
alphabeticalByTypeOrderCode,
];

@override
void run(
CustomLintResolver resolver,
DiagnosticReporter reporter,
CustomLintContext context,
void registerNodeProcessors(
RuleVisitorRegistry registry,
RuleContext context,
) {
context.registry.addClassDeclaration((node) {
final visitor = MemberOrderingVisitor(
config.parameters.groupsOrder,
config.parameters.widgetsGroupsOrder,
);

final membersInfo = visitor.visitClassDeclaration(node);
final wrongOrderMembers = membersInfo.where(
(info) => info.memberOrder.isWrong,
);

for (final memberInfo in wrongOrderMembers) {
reporter.atNode(
memberInfo.classMember,
_createWrongOrderLintCode(memberInfo),
);
}

if (config.parameters.alphabetize) {
final alphabeticallyWrongOrderMembers = membersInfo.where(
(info) => info.memberOrder.isAlphabeticallyWrong,
);

for (final memberInfo in alphabeticallyWrongOrderMembers) {
reporter.atNode(
memberInfo.classMember,
_createAlphabeticallyWrongOrderLintCode(memberInfo),
);
}
}

if (!config.parameters.alphabetize &&
config.parameters.alphabetizeByType) {
final alphabeticallyByTypeWrongOrderMembers = membersInfo.where(
(info) => info.memberOrder.isByTypeWrong,
);

for (final memberInfo in alphabeticallyByTypeWrongOrderMembers) {
reporter.atNode(
memberInfo.classMember,
_createAlphabeticallyByTypeWrongOrderLintCode(memberInfo),
);
}
}
});
}

LintCode _createWrongOrderLintCode(MemberInfo info) {
final memberGroup = info.memberOrder.memberGroup;
final previousMemberGroup = info.memberOrder.previousMemberGroup;

return LintCode(
name: lintName,
problemMessage: "$memberGroup $_warningMessage $previousMemberGroup.",
);
}

LintCode _createAlphabeticallyWrongOrderLintCode(MemberInfo info) {
final names = info.memberOrder.memberNames;
final current = names.currentName;
final previous = names.previousName;

return LintCode(
name: lintName,
problemMessage: "$current $_warningAlphabeticalMessage $previous.",
);
}
super.registerNodeProcessors(registry, context);

LintCode _createAlphabeticallyByTypeWrongOrderLintCode(MemberInfo info) {
final names = info.memberOrder.memberNames;
final current = names.currentName;
final previous = names.previousName;
final parameters =
getParametersForContext(context) ?? MemberOrderingParameters.empty();
final visitor = MemberOrderingVisitor(this, parameters);

return LintCode(
name: lintName,
problemMessage: "$current $_warningTypeAlphabeticalMessage $previous",
);
registry.addClassDeclaration(this, visitor);
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I played around refactoring it and I was able to get rid of this file completely:

lib/src/utils/implies.dart:

/// Logical implication operation.
extension BoolImplies on bool {
  /// Logical implication operation.
  ///
  /// True implies that [other] is also true.
  // ignore: avoid_positional_boolean_parameters
  bool implies(bool other) => objectImplies(this, other, false);
}

/// Sugar syntax helper to write extensions analogus to bool implies.
///
/// A truthy (!= [falsy]) value of [a] implies an equal value of [b].
bool objectImplies<T>(T a, T b, T falsy) => a == falsy || a == b;

/// A class that supports logical implication of it's objects.
abstract class Implicable {
  /// Logical implication operation.
  ///
  /// A truthy value of this object implies an equal value of [other].
  bool implies(Implicable other);
}

Add a new method to abstract class MemberGroup:

  abstract class MemberGroup implements Implicable {

...

  @override
  bool implies(covariant MemberGroup other) =>
      modifier.implies(other.modifier) && annotation.implies(other.annotation);
}

and same for subclasses and relevant enums:

  @override
  bool implies(covariant ConstructorMemberGroup other) =>
      isFactory.implies(other.isFactory) && isNamed.implies(other.isNamed);
  @override
  bool implies(covariant FieldMemberGroup other) =>
      isLate.implies(other.isLate) &&
      isStatic.implies(other.isStatic) &&
      isNullable.implies(other.isNullable) &&
      keyword.implies(other.keyword);
  @override
  bool implies(covariant Modifier other) =>
      objectImplies(this, other, Modifier.unset);
  @override
  bool implies(covariant Annotation other) =>
      objectImplies(this, other, Annotation.unset);
  @override
  bool implies(covariant FieldKeyword other) =>
      objectImplies(this, other, FieldKeyword.unset);
  @override
  bool implies(MemberGroup other) =>
      super.implies(other) &&
      other is MethodMemberGroup &&
      isStatic.implies(other.isStatic) &&
      isNullable.implies(other.isNullable) &&
      name.implies(other.name);
}

extension on String? {
  bool implies(String? other) => objectImplies(this, other, null);
}
  @override
  bool implies(covariant FieldMemberGroup other) =>
      isLate.implies(other.isLate) &&
      isStatic.implies(other.isStatic) &&
      isNullable.implies(other.isNullable) &&
      keyword.implies(other.keyword);

One thing I'm not entirely sure about is covariant - maybe we should just check is in the method instead.

WDYT?

Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import 'package:solid_lints/src/lints/member_ordering/models/annotation.dart';
import 'package:solid_lints/src/lints/member_ordering/models/field_keyword.dart';
import 'package:solid_lints/src/lints/member_ordering/models/member_group/constructor_member_group.dart';
import 'package:solid_lints/src/lints/member_ordering/models/member_group/field_member_group.dart';
import 'package:solid_lints/src/lints/member_ordering/models/member_group/get_set_member_group.dart';
import 'package:solid_lints/src/lints/member_ordering/models/member_group/member_group.dart';
import 'package:solid_lints/src/lints/member_ordering/models/member_group/method_member_group.dart';
import 'package:solid_lints/src/lints/member_ordering/models/member_type.dart';
import 'package:solid_lints/src/lints/member_ordering/models/modifier.dart';

/// Extension methods for [MemberGroup] to check if a parsed group matches
/// the properties of this group.
extension MemberGroupExtensions on MemberGroup {
/// Checks whether this [MemberGroup] satisfies the properties
/// of the [parsedGroup].
bool satisfies(MemberGroup parsedGroup) {
if (!_matchesBaseProperties(parsedGroup)) {
return false;
}

final group = this;
return switch ((group, parsedGroup)) {
(final ConstructorMemberGroup g, final ConstructorMemberGroup p) =>
_isConstructor(g, p),
(final MethodMemberGroup g, final MethodMemberGroup p) => _isMethod(g, p),
(final GetSetMemberGroup g, final GetSetMemberGroup p) => _isGetSet(g, p),
(final FieldMemberGroup g, final FieldMemberGroup p) => _isField(g, p),
_ => false,
};
}

bool _matchesBaseProperties(MemberGroup parsedGroup) =>
(modifier == Modifier.unset || modifier == parsedGroup.modifier) &&
(annotation == Annotation.unset || annotation == parsedGroup.annotation);

bool _isConstructor(
ConstructorMemberGroup g,
ConstructorMemberGroup p,
) =>
(!g.isFactory || g.isFactory == p.isFactory) &&
(!g.isNamed || g.isNamed == p.isNamed);

bool _isMethod(MethodMemberGroup g, MethodMemberGroup p) =>
(!g.isStatic || g.isStatic == p.isStatic) &&
(!g.isNullable || g.isNullable == p.isNullable) &&
(g.name == null || g.name == p.name);

bool _isGetSet(GetSetMemberGroup g, GetSetMemberGroup p) =>
(g.memberType == p.memberType ||
(g.memberType == MemberType.getterAndSetter &&
(p.memberType == MemberType.getter ||
p.memberType == MemberType.setter))) &&
(!g.isStatic || g.isStatic == p.isStatic) &&
(!g.isNullable || g.isNullable == p.isNullable);

bool _isField(FieldMemberGroup g, FieldMemberGroup p) =>
(!g.isLate || g.isLate == p.isLate) &&
(!g.isStatic || g.isStatic == p.isStatic) &&
(!g.isNullable || g.isNullable == p.isNullable) &&
(g.keyword == FieldKeyword.unset || g.keyword == p.keyword);
}
17 changes: 17 additions & 0 deletions lib/src/lints/member_ordering/models/member_info.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import 'package:analyzer/dart/ast/ast.dart';
import 'package:solid_lints/src/lints/member_ordering/models/member_order.dart';

/// Data class that holds AST class member and it's order info
class MemberInfo {
/// AST instance of an [ClassMember]
final ClassMember classMember;

/// Class member order info
final MemberOrder memberOrder;

/// Creates instance of an [MemberInfo]
const MemberInfo({
required this.classMember,
required this.memberOrder,
});
}
22 changes: 22 additions & 0 deletions lib/src/lints/member_ordering/models/member_names.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/// Data class contains info about current and previous class member names
class MemberNames {
/// Name of current class member
final String currentName;

/// Name of previous class member
final String? previousName;

/// Type name of current class member
final String currentTypeName;

/// Type name of previous class member
final String? previousTypeName;

/// Creates instance of [MemberNames]
const MemberNames({
required this.currentName,
required this.currentTypeName,
this.previousName,
this.previousTypeName,
});
}
33 changes: 33 additions & 0 deletions lib/src/lints/member_ordering/models/member_order.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import 'package:solid_lints/src/lints/member_ordering/models/member_group/member_group.dart';
import 'package:solid_lints/src/lints/member_ordering/models/member_names.dart';

/// Data class holds information about class member order info
class MemberOrder {
/// Indicates if order is wrong
final bool isWrong;

/// Indicates if order is wrong alphabetically
final bool isAlphabeticallyWrong;

/// Indicates if order is wrong alphabetically by type
final bool isByTypeWrong;

/// Info about current and previous class member name
final MemberNames memberNames;

/// Info about current member member group
final MemberGroup memberGroup;

/// Info about previous member member group
final MemberGroup? previousMemberGroup;

/// Creates instance of [MemberOrder]
const MemberOrder({
required this.isWrong,
required this.isAlphabeticallyWrong,
required this.isByTypeWrong,
required this.memberNames,
required this.memberGroup,
this.previousMemberGroup,
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,14 @@ class MemberOrderingParameters {
required this.alphabetizeByType,
});

/// Factory for creating empty/default parameters.
factory MemberOrderingParameters.empty() => MemberOrderingParameters(
groupsOrder: MemberOrderingConfigParser.parseOrder(null),
widgetsGroupsOrder: MemberOrderingConfigParser.parseWidgetsOrder(null),
alphabetize: false,
alphabetizeByType: false,
);

/// Method for creating from json data
factory MemberOrderingParameters.fromJson(Map<String, Object?> json) =>
MemberOrderingParameters(
Expand Down
2 changes: 1 addition & 1 deletion lib/src/lints/member_ordering/models/member_type.dart
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ enum MemberType {
method('methods', typeAlias: 'method'),

/// Indicates constructor affiliation
constructor('constructors'),
constructor('constructors', typeAlias: 'constructor'),

/// Indicates getters affiliation
getter('getters'),
Expand Down
Loading