Skip to content

Commit

Permalink
Feature/member ordering (#51)
Browse files Browse the repository at this point in the history
* Add required models for member-ordering rule

* Add member-ordering rule

* Add MIT License comments

* Fix parser type mismatch

* Add tests for member-ordering and enable rule

* Organize imports, ignore member-ordering in unrelated test

* Group *_member_group.dart into one directory

* Add more test cases

* Add more test cases

* Add alphabetize test cases

* Add tests for alphabetical-by-type option

* Ignore no-empty-block in test

* Ignore member-ordering in test

---------

Co-authored-by: Denis Bogatirov <[email protected]>
  • Loading branch information
DenisBogatirov and Denis Bogatirov authored Sep 15, 2023
1 parent 394c02f commit 6f29f0e
Show file tree
Hide file tree
Showing 24 changed files with 1,672 additions and 0 deletions.
167 changes: 167 additions & 0 deletions lib/lints/member_ordering/config_parser.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
// MIT License
//
// Copyright (c) 2020-2021 Dart Code Checker team
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.

import 'package:collection/collection.dart';
import 'package:solid_lints/lints/member_ordering/models/annotation.dart';
import 'package:solid_lints/lints/member_ordering/models/field_keyword.dart';
import 'package:solid_lints/lints/member_ordering/models/member_group/constructor_member_group.dart';
import 'package:solid_lints/lints/member_ordering/models/member_group/field_member_group.dart';
import 'package:solid_lints/lints/member_ordering/models/member_group/get_set_member_group.dart';
import 'package:solid_lints/lints/member_ordering/models/member_group/member_group.dart';
import 'package:solid_lints/lints/member_ordering/models/member_group/method_member_group.dart';
import 'package:solid_lints/lints/member_ordering/models/member_type.dart';
import 'package:solid_lints/lints/member_ordering/models/modifier.dart';

/// Helper class to parse member-ordering rule config
class MemberOrderingConfigParser {
static const _defaultOrderList = [
'public-fields',
'private-fields',
'public-getters',
'private-getters',
'public-setters',
'private-setters',
'constructors',
'public-methods',
'private-methods',
];

static const _defaultWidgetsOrderList = [
'constructor',
'named-constructor',
'const-fields',
'static-methods',
'final-fields',
'init-state-method',
'var-fields',
'init-state-method',
'private-methods',
'overridden-public-methods',
'build-method',
];

static final _regExp = RegExp(
'(overridden-|protected-)?(private-|public-)?(static-)?(late-)?'
'(var-|final-|const-)?(nullable-)?(named-)?(factory-)?',
);

/// Parse rule config for regular class order rules
static List<MemberGroup> parseOrder(Object? orderConfig) {
final order = orderConfig is Iterable
? List<String>.from(orderConfig)
: _defaultOrderList;

return order.map(_parseGroup).whereNotNull().toList();
}

/// Parse rule config for widget class order rules
static List<MemberGroup> parseWidgetsOrder(Object? widgetsOrderConfig) {
final widgetsOrder = widgetsOrderConfig is Iterable
? List<String>.from(widgetsOrderConfig)
: _defaultWidgetsOrderList;

return widgetsOrder.map(_parseGroup).whereNotNull().toList();
}

static MemberGroup? _parseGroup(String group) {
final lastGroup = group.endsWith('getters-setters')
? 'getters-setters'
: group.split('-').lastOrNull;
final type = MemberType.parse(lastGroup);
final result = _regExp.allMatches(group.toLowerCase());

final isNamedMethod = group.endsWith('-method');
if (isNamedMethod) {
final name = group.split('-method').first.replaceAll('-', '');

return MethodMemberGroup.named(
name: name,
memberType: MemberType.method,
rawRepresentation: group,
);
}

final hasGroups = result.isNotEmpty && result.first.groupCount > 0;
if (hasGroups && type != null) {
final match = result.first;

final annotation = Annotation.parse(match.group(1)?.replaceAll('-', ''));
final modifier = Modifier.parse(match.group(2)?.replaceAll('-', ''));
final isStatic = match.group(3) != null;
final isLate = match.group(4) != null;
final keyword = FieldKeyword.parse(match.group(5)?.replaceAll('-', ''));
final isNullable = match.group(6) != null;
final isNamed = match.group(7) != null;
final isFactory = match.group(8) != null;

switch (type) {
case MemberType.field:
return FieldMemberGroup(
isLate: isLate,
isNullable: isNullable,
isStatic: isStatic,
keyword: keyword,
annotation: annotation,
memberType: type,
modifier: modifier,
rawRepresentation: group,
);

case MemberType.method:
return MethodMemberGroup(
name: null,
isNullable: isNullable,
isStatic: isStatic,
annotation: annotation,
memberType: type,
modifier: modifier,
rawRepresentation: group,
);

case MemberType.getter:
case MemberType.setter:
case MemberType.getterAndSetter:
return GetSetMemberGroup(
isNullable: isNullable,
isStatic: isStatic,
annotation: annotation,
memberType: type,
modifier: modifier,
rawRepresentation: group,
);

case MemberType.constructor:
return ConstructorMemberGroup(
isNamed: isFactory || isNamed,
isFactory: isFactory,
annotation: annotation,
memberType: type,
modifier: modifier,
rawRepresentation: group,
);
}
}

return null;
}
}
120 changes: 120 additions & 0 deletions lib/lints/member_ordering/member_ordering_rule.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import 'package:analyzer/error/listener.dart';
import 'package:custom_lint_builder/custom_lint_builder.dart';
import 'package:solid_lints/lints/member_ordering/member_ordering_visitor.dart';
import 'package:solid_lints/lints/member_ordering/models/member_ordering_parameters.dart';
import 'package:solid_lints/models/rule_config.dart';
import 'package:solid_lints/models/solid_lint_rule.dart';

/// A `member-ordering` rule which
/// warns about class members being in wrong order
/// Custom order can be provided through config
class MemberOrderingRule extends SolidLintRule<MemberOrderingParameters> {
/// The [LintCode] of this lint rule that represents
/// the error whether we use bad formatted double literals.
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';

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
void run(
CustomLintResolver resolver,
ErrorReporter reporter,
CustomLintContext 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.reportErrorForNode(
_createWrongOrderLintCode(memberInfo),
memberInfo.classMember,
);
}

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

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

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

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

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.",
);
}

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

return LintCode(
name: lintName,
problemMessage: "$current $_warningTypeAlphabeticalMessage $previous",
);
}
}
21 changes: 21 additions & 0 deletions lib/lints/member_ordering/member_ordering_utils.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import 'package:analyzer/dart/ast/ast.dart' show AnnotatedNode, Identifier;
import 'package:collection/collection.dart';
import 'package:solid_lints/lints/member_ordering/models/annotation.dart';
import 'package:solid_lints/lints/member_ordering/models/modifier.dart';

/// Parses [AnnotatedNode] and creates an instance of [Annotation]
Annotation? parseAnnotation(AnnotatedNode node) {
return node.metadata
.map((metadata) => Annotation.parse(metadata.name.name))
.whereNotNull()
.firstOrNull;
}

/// Parses class memberName and return it's access [Modifier]
Modifier parseModifier(String? memberName) {
if (memberName == null) return Modifier.unset;

return Identifier.isPrivateName(memberName)
? Modifier.private
: Modifier.public;
}
Loading

0 comments on commit 6f29f0e

Please sign in to comment.