diff --git a/lib/lints/member_ordering/config_parser.dart b/lib/lints/member_ordering/config_parser.dart new file mode 100644 index 00000000..9e42421f --- /dev/null +++ b/lib/lints/member_ordering/config_parser.dart @@ -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 parseOrder(Object? orderConfig) { + final order = orderConfig is Iterable + ? List.from(orderConfig) + : _defaultOrderList; + + return order.map(_parseGroup).whereNotNull().toList(); + } + + /// Parse rule config for widget class order rules + static List parseWidgetsOrder(Object? widgetsOrderConfig) { + final widgetsOrder = widgetsOrderConfig is Iterable + ? List.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; + } +} diff --git a/lib/lints/member_ordering/member_ordering_rule.dart b/lib/lints/member_ordering/member_ordering_rule.dart new file mode 100644 index 00000000..c922d8a5 --- /dev/null +++ b/lib/lints/member_ordering/member_ordering_rule.dart @@ -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 { + /// 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( + 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", + ); + } +} diff --git a/lib/lints/member_ordering/member_ordering_utils.dart b/lib/lints/member_ordering/member_ordering_utils.dart new file mode 100644 index 00000000..d1821f79 --- /dev/null +++ b/lib/lints/member_ordering/member_ordering_utils.dart @@ -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; +} diff --git a/lib/lints/member_ordering/member_ordering_visitor.dart b/lib/lints/member_ordering/member_ordering_visitor.dart new file mode 100644 index 00000000..071a9c7b --- /dev/null +++ b/lib/lints/member_ordering/member_ordering_visitor.dart @@ -0,0 +1,362 @@ +// 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:analyzer/dart/ast/ast.dart' hide Annotation; +import 'package:analyzer/dart/ast/visitor.dart'; +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'; +import 'package:solid_lints/utils/types_utils.dart'; + +/// AST Visitor which finds all class members and checks if they are +/// in order provided from rule config or default config +class MemberOrderingVisitor extends RecursiveAstVisitor> { + final List _groupsOrder; + final List _widgetsGroupsOrder; + + final _membersInfo = []; + + /// Creates instance of [MemberOrderingVisitor] + /// [_groupsOrder] config is used for regular classes + /// [_widgetsGroupsOrder] config is used for widget classes + MemberOrderingVisitor(this._groupsOrder, this._widgetsGroupsOrder); + + @override + List visitClassDeclaration(ClassDeclaration node) { + super.visitClassDeclaration(node); + + _membersInfo.clear(); + + final type = node.extendsClause?.superclass.type; + final isFlutterWidget = + isWidgetOrSubclass(type) || isWidgetStateOrSubclass(type); + + for (final member in node.members) { + if (member is FieldDeclaration) { + _visitFieldDeclaration(member, isFlutterWidget); + } else if (member is ConstructorDeclaration) { + _visitConstructorDeclaration(member, isFlutterWidget); + } else if (member is MethodDeclaration) { + _visitMethodDeclaration(member, isFlutterWidget); + } + } + + return _membersInfo; + } + + void _visitFieldDeclaration( + FieldDeclaration declaration, + bool isFlutterWidget, + ) { + final group = FieldMemberGroup.parse(declaration); + final closestGroup = _getClosestGroup(group, isFlutterWidget); + + if (closestGroup != null) { + _membersInfo.add( + MemberInfo( + classMember: declaration, + memberOrder: _getOrder( + closestGroup, + declaration.fields.variables.first.name.lexeme, + declaration.fields.type?.type + ?.getDisplayString(withNullability: false) ?? + '_', + isFlutterWidget, + ), + ), + ); + } + } + + void _visitConstructorDeclaration( + ConstructorDeclaration declaration, + bool isFlutterWidget, + ) { + final group = ConstructorMemberGroup.parse(declaration); + final closestGroup = _getClosestGroup(group, isFlutterWidget); + + if (closestGroup != null) { + _membersInfo.add( + MemberInfo( + classMember: declaration, + memberOrder: _getOrder( + closestGroup, + declaration.name?.lexeme ?? '', + declaration.returnType.name, + isFlutterWidget, + ), + ), + ); + } + } + + void _visitMethodDeclaration( + MethodDeclaration declaration, + bool isFlutterWidget, + ) { + if (declaration.isGetter || declaration.isSetter) { + final group = GetSetMemberGroup.parse(declaration); + final closestGroup = _getClosestGroup(group, isFlutterWidget); + + if (closestGroup != null) { + _membersInfo.add( + MemberInfo( + classMember: declaration, + memberOrder: _getOrder( + closestGroup, + declaration.name.lexeme, + declaration.returnType?.type + ?.getDisplayString(withNullability: false) ?? + '_', + isFlutterWidget, + ), + ), + ); + } + } else { + final group = MethodMemberGroup.parse(declaration); + final closestGroup = _getClosestGroup(group, isFlutterWidget); + + if (closestGroup != null) { + _membersInfo.add( + MemberInfo( + classMember: declaration, + memberOrder: _getOrder( + closestGroup, + declaration.name.lexeme, + declaration.returnType?.type + ?.getDisplayString(withNullability: false) ?? + '_', + isFlutterWidget, + ), + ), + ); + } + } + } + + MemberGroup? _getClosestGroup( + MemberGroup parsedGroup, + bool isFlutterWidget, + ) { + final closestGroups = (isFlutterWidget ? _widgetsGroupsOrder : _groupsOrder) + .where( + (group) => + _isConstructorGroup(group, parsedGroup) || + _isFieldGroup(group, parsedGroup) || + _isGetSetGroup(group, parsedGroup) || + _isMethodGroup(group, parsedGroup), + ) + .sorted( + (a, b) => b.getSortingCoefficient() - a.getSortingCoefficient(), + ); + + return closestGroups.firstOrNull; + } + + MemberOrder _getOrder( + MemberGroup memberGroup, + String memberName, + String typeName, + bool isFlutterWidget, + ) { + if (_membersInfo.isNotEmpty) { + final lastMemberOrder = _membersInfo.last.memberOrder; + final hasSameGroup = lastMemberOrder.memberGroup == memberGroup; + + final previousMemberGroup = + hasSameGroup && lastMemberOrder.previousMemberGroup != null + ? lastMemberOrder.previousMemberGroup + : lastMemberOrder.memberGroup; + + final memberNames = MemberNames( + currentName: memberName, + previousName: lastMemberOrder.memberNames.currentName, + currentTypeName: typeName, + previousTypeName: lastMemberOrder.memberNames.currentTypeName, + ); + + return MemberOrder( + memberNames: memberNames, + isAlphabeticallyWrong: hasSameGroup && + memberNames.currentName.compareTo(memberNames.previousName!) < 0, + isByTypeWrong: hasSameGroup && + memberNames.currentTypeName + .toLowerCase() + .compareTo(memberNames.previousTypeName!.toLowerCase()) < + 0, + memberGroup: memberGroup, + previousMemberGroup: previousMemberGroup, + isWrong: (hasSameGroup && lastMemberOrder.isWrong) || + _isCurrentGroupBefore( + lastMemberOrder.memberGroup, + memberGroup, + isFlutterWidget, + ), + ); + } + + return MemberOrder( + memberNames: + MemberNames(currentName: memberName, currentTypeName: typeName), + isAlphabeticallyWrong: false, + isByTypeWrong: false, + memberGroup: memberGroup, + isWrong: false, + ); + } + + bool _isCurrentGroupBefore( + MemberGroup lastMemberGroup, + MemberGroup memberGroup, + bool isFlutterWidget, + ) { + final group = isFlutterWidget ? _widgetsGroupsOrder : _groupsOrder; + + return group.indexOf(lastMemberGroup) > group.indexOf(memberGroup); + } + + bool _isConstructorGroup(MemberGroup group, MemberGroup parsedGroup) => + group is ConstructorMemberGroup && + parsedGroup is ConstructorMemberGroup && + (!group.isFactory || group.isFactory == parsedGroup.isFactory) && + (!group.isNamed || group.isNamed == parsedGroup.isNamed) && + (group.modifier == Modifier.unset || + group.modifier == parsedGroup.modifier) && + (group.annotation == Annotation.unset || + group.annotation == parsedGroup.annotation); + + bool _isMethodGroup(MemberGroup group, MemberGroup parsedGroup) => + group is MethodMemberGroup && + parsedGroup is MethodMemberGroup && + (!group.isStatic || group.isStatic == parsedGroup.isStatic) && + (!group.isNullable || group.isNullable == parsedGroup.isNullable) && + (group.name == null || group.name == parsedGroup.name) && + (group.modifier == Modifier.unset || + group.modifier == parsedGroup.modifier) && + (group.annotation == Annotation.unset || + group.annotation == parsedGroup.annotation); + + bool _isGetSetGroup(MemberGroup group, MemberGroup parsedGroup) => + group is GetSetMemberGroup && + parsedGroup is GetSetMemberGroup && + (group.memberType == parsedGroup.memberType || + (group.memberType == MemberType.getterAndSetter && + (parsedGroup.memberType == MemberType.getter || + parsedGroup.memberType == MemberType.setter))) && + (!group.isStatic || group.isStatic == parsedGroup.isStatic) && + (!group.isNullable || group.isNullable == parsedGroup.isNullable) && + (group.modifier == Modifier.unset || + group.modifier == parsedGroup.modifier) && + (group.annotation == Annotation.unset || + group.annotation == parsedGroup.annotation); + + bool _isFieldGroup(MemberGroup group, MemberGroup parsedGroup) => + group is FieldMemberGroup && + parsedGroup is FieldMemberGroup && + (!group.isLate || group.isLate == parsedGroup.isLate) && + (!group.isStatic || group.isStatic == parsedGroup.isStatic) && + (!group.isNullable || group.isNullable == parsedGroup.isNullable) && + (group.modifier == Modifier.unset || + group.modifier == parsedGroup.modifier) && + (group.keyword == FieldKeyword.unset || + group.keyword == parsedGroup.keyword) && + (group.annotation == Annotation.unset || + group.annotation == parsedGroup.annotation); +} + +/// 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, + }); +} + +/// 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, + }); +} + +/// 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; + + /// Crates instance of [MemberNames] + const MemberNames({ + required this.currentName, + required this.currentTypeName, + this.previousName, + this.previousTypeName, + }); +} diff --git a/lib/lints/member_ordering/models/annotation.dart b/lib/lints/member_ordering/models/annotation.dart new file mode 100644 index 00000000..1e16249c --- /dev/null +++ b/lib/lints/member_ordering/models/annotation.dart @@ -0,0 +1,51 @@ +// 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. + +/// A data model enum represents annotation +enum Annotation { + /// override annotation + override('override', 'overridden'), + + /// protected annotation + protected('protected'), + + /// Indicates missing annotation + /// used to handle cases of unsupported annotations + unset('unset'); + + /// String representation of name of an annotation + final String name; + + /// String representation of public name of an annotation + final String? publicName; + + const Annotation(this.name, [this.publicName]); + + /// Parses a String name and returns instance of [Annotation] + static Annotation parse(String? name) => values.firstWhere( + (annotation) => + annotation.name == name || + (annotation.publicName != null && annotation.publicName == name), + orElse: () => Annotation.unset, + ); +} diff --git a/lib/lints/member_ordering/models/field_keyword.dart b/lib/lints/member_ordering/models/field_keyword.dart new file mode 100644 index 00000000..d2716ca2 --- /dev/null +++ b/lib/lints/member_ordering/models/field_keyword.dart @@ -0,0 +1,49 @@ +// 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. + +/// A data model enum represents field type keyword +enum FieldKeyword { + /// final keyword + isFinal('final'), + + /// const keyword + isConst('const'), + + /// var keyword + isVar('var'), + + /// Indicates missing field keyword + /// used to handle cases of unsupported field type keywords + unset('unset'); + + /// String representation of field type keyword + final String type; + + const FieldKeyword(this.type); + + /// Parses a String field type and returns instance of [FieldKeyword] + static FieldKeyword parse(String? name) => values.firstWhere( + (type) => type.type == name, + orElse: () => FieldKeyword.unset, + ); +} diff --git a/lib/lints/member_ordering/models/member_group/constructor_member_group.dart b/lib/lints/member_ordering/models/member_group/constructor_member_group.dart new file mode 100644 index 00000000..7086a74c --- /dev/null +++ b/lib/lints/member_ordering/models/member_group/constructor_member_group.dart @@ -0,0 +1,83 @@ +// 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:analyzer/dart/ast/ast.dart' show ConstructorDeclaration; +import 'package:solid_lints/lints/member_ordering/member_ordering_utils.dart'; +import 'package:solid_lints/lints/member_ordering/models/annotation.dart'; +import 'package:solid_lints/lints/member_ordering/models/member_group/member_group.dart'; +import 'package:solid_lints/lints/member_ordering/models/member_type.dart'; +import 'package:solid_lints/lints/member_ordering/models/modifier.dart'; + +/// Data class represents class constructor +class ConstructorMemberGroup extends MemberGroup { + /// Shows if constructor is named + final bool isNamed; + + /// Shows if constructor is factory constructor + final bool isFactory; + + /// Creates instance of [ConstructorMemberGroup] + const ConstructorMemberGroup({ + required this.isNamed, + required this.isFactory, + required super.annotation, + required super.memberType, + required super.modifier, + required super.rawRepresentation, + }); + + /// Parses instance of [ConstructorDeclaration] + /// and returns [ConstructorMemberGroup] + factory ConstructorMemberGroup.parse(ConstructorDeclaration declaration) { + final annotation = parseAnnotation(declaration); + final name = declaration.name; + final isFactory = declaration.factoryKeyword != null; + final isNamed = name != null; + + final modifier = parseModifier(name?.lexeme); + + return ConstructorMemberGroup( + isNamed: isNamed, + isFactory: isFactory, + annotation: annotation ?? Annotation.unset, + modifier: modifier, + memberType: MemberType.constructor, + rawRepresentation: '', + ); + } + + @override + int getSortingCoefficient() { + var coefficient = 0; + + coefficient += isNamed ? 1 : 0; + coefficient += isFactory ? 1 : 0; + coefficient += annotation != Annotation.unset ? 1 : 0; + coefficient += modifier != Modifier.unset ? 1 : 0; + + return coefficient; + } + + @override + String toString() => rawRepresentation; +} diff --git a/lib/lints/member_ordering/models/member_group/field_member_group.dart b/lib/lints/member_ordering/models/member_group/field_member_group.dart new file mode 100644 index 00000000..e1a223ec --- /dev/null +++ b/lib/lints/member_ordering/models/member_group/field_member_group.dart @@ -0,0 +1,107 @@ +// 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:analyzer/dart/ast/ast.dart' show FieldDeclaration; +import 'package:analyzer/dart/element/nullability_suffix.dart'; +import 'package:solid_lints/lints/member_ordering/member_ordering_utils.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/member_group.dart'; +import 'package:solid_lints/lints/member_ordering/models/member_type.dart'; +import 'package:solid_lints/lints/member_ordering/models/modifier.dart'; + +/// Data class represents class field +class FieldMemberGroup extends MemberGroup { + /// Shows if field is static member of class + final bool isStatic; + + /// Shows if field is nullable type + final bool isNullable; + + /// Show if field is late initialization + final bool isLate; + + /// Represents field keyword + final FieldKeyword keyword; + + /// Creates instance of [FieldMemberGroup] + const FieldMemberGroup({ + required this.isLate, + required this.isNullable, + required this.isStatic, + required this.keyword, + required super.annotation, + required super.memberType, + required super.modifier, + required super.rawRepresentation, + }); + + /// Parses [FieldDeclaration] and creates instance of [FieldMemberGroup] + factory FieldMemberGroup.parse(FieldDeclaration declaration) { + final annotation = parseAnnotation(declaration); + final modifier = parseModifier( + declaration.fields.variables.first.name.lexeme, + ); + final isNullable = declaration.fields.type?.type?.nullabilitySuffix == + NullabilitySuffix.question; + final keyword = _FieldMemberGroupUtils.parseKeyWord(declaration); + + return FieldMemberGroup( + annotation: annotation ?? Annotation.unset, + isStatic: declaration.isStatic, + isNullable: isNullable, + isLate: declaration.fields.isLate, + memberType: MemberType.field, + modifier: modifier, + keyword: keyword, + rawRepresentation: '', + ); + } + + @override + int getSortingCoefficient() { + var coefficient = 0; + + coefficient += isStatic ? 1 : 0; + coefficient += isNullable ? 1 : 0; + coefficient += isLate ? 1 : 0; + coefficient += keyword != FieldKeyword.unset ? 1 : 0; + coefficient += annotation != Annotation.unset ? 1 : 0; + coefficient += modifier != Modifier.unset ? 1 : 0; + + return coefficient; + } + + @override + String toString() => rawRepresentation; +} + +class _FieldMemberGroupUtils { + static FieldKeyword parseKeyWord(FieldDeclaration declaration) { + return declaration.fields.isConst + ? FieldKeyword.isConst + : declaration.fields.isFinal + ? FieldKeyword.isFinal + : FieldKeyword.unset; + } +} diff --git a/lib/lints/member_ordering/models/member_group/get_set_member_group.dart b/lib/lints/member_ordering/models/member_group/get_set_member_group.dart new file mode 100644 index 00000000..abd16b4e --- /dev/null +++ b/lib/lints/member_ordering/models/member_group/get_set_member_group.dart @@ -0,0 +1,79 @@ +// 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:analyzer/dart/ast/ast.dart' show MethodDeclaration; +import 'package:solid_lints/lints/member_ordering/member_ordering_utils.dart'; +import 'package:solid_lints/lints/member_ordering/models/annotation.dart'; +import 'package:solid_lints/lints/member_ordering/models/member_group/member_group.dart'; +import 'package:solid_lints/lints/member_ordering/models/member_type.dart'; +import 'package:solid_lints/lints/member_ordering/models/modifier.dart'; + +/// Data class represents class getter or setter +class GetSetMemberGroup extends MemberGroup { + /// Shows if getter/setter is static class member + final bool isStatic; + + /// Show if return type of getter/setter is nullable type + final bool isNullable; + + /// Creates instance of [GetSetMemberGroup] + const GetSetMemberGroup({ + required this.isNullable, + required this.isStatic, + required super.annotation, + required super.memberType, + required super.modifier, + required super.rawRepresentation, + }); + + /// Parses [MethodDeclaration] and returns instance of [GetSetMemberGroup] + factory GetSetMemberGroup.parse(MethodDeclaration declaration) { + final annotation = parseAnnotation(declaration); + final type = declaration.isGetter ? MemberType.getter : MemberType.setter; + final modifier = parseModifier(declaration.name.lexeme); + + return GetSetMemberGroup( + annotation: annotation ?? Annotation.unset, + isStatic: declaration.isStatic, + isNullable: declaration.returnType?.question != null, + memberType: type, + modifier: modifier, + rawRepresentation: '', + ); + } + + @override + int getSortingCoefficient() { + var coefficient = 0; + + coefficient += isStatic ? 1 : 0; + coefficient += isNullable ? 1 : 0; + coefficient += annotation != Annotation.unset ? 1 : 0; + coefficient += modifier != Modifier.unset ? 1 : 0; + + return coefficient; + } + + @override + String toString() => rawRepresentation; +} diff --git a/lib/lints/member_ordering/models/member_group/member_group.dart b/lib/lints/member_ordering/models/member_group/member_group.dart new file mode 100644 index 00000000..b35990d7 --- /dev/null +++ b/lib/lints/member_ordering/models/member_group/member_group.dart @@ -0,0 +1,52 @@ +// 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:solid_lints/lints/member_ordering/models/annotation.dart'; +import 'package:solid_lints/lints/member_ordering/models/member_type.dart'; +import 'package:solid_lints/lints/member_ordering/models/modifier.dart'; + +/// Abstract class representing class member group +abstract class MemberGroup { + /// Member annotation (e.g. override, protected) + final Annotation annotation; + + /// Member type (e.g. field, method) + final MemberType memberType; + + /// Member access modifier (e.g. public, private) + final Modifier modifier; + + /// Raw String representation of class member group + final String rawRepresentation; + + /// Constructor to use in children extending [MemberGroup] + const MemberGroup({ + required this.annotation, + required this.memberType, + required this.modifier, + required this.rawRepresentation, + }); + + /// Method to get sorting coefficient of the member group + int getSortingCoefficient(); +} diff --git a/lib/lints/member_ordering/models/member_group/method_member_group.dart b/lib/lints/member_ordering/models/member_group/method_member_group.dart new file mode 100644 index 00000000..9a0ac33e --- /dev/null +++ b/lib/lints/member_ordering/models/member_group/method_member_group.dart @@ -0,0 +1,103 @@ +// 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:analyzer/dart/ast/ast.dart' show Identifier, MethodDeclaration; +import 'package:solid_lints/lints/member_ordering/member_ordering_utils.dart'; +import 'package:solid_lints/lints/member_ordering/models/annotation.dart'; +import 'package:solid_lints/lints/member_ordering/models/member_group/member_group.dart'; +import 'package:solid_lints/lints/member_ordering/models/member_type.dart'; +import 'package:solid_lints/lints/member_ordering/models/modifier.dart'; + +/// Data class represents class method +class MethodMemberGroup extends MemberGroup { + /// Shows if method is static member of a class + final bool isStatic; + + /// Shows if method return type is nullable + final bool isNullable; + + /// Represents method name + final String? name; + + /// Creates instance of [MethodMemberGroup] + const MethodMemberGroup({ + required this.isNullable, + required this.isStatic, + required this.name, + required super.annotation, + required super.memberType, + required super.modifier, + required super.rawRepresentation, + }); + + /// Named constructor to create an instance of [MethodMemberGroup] with name + factory MethodMemberGroup.named({ + required String name, + required MemberType memberType, + required String rawRepresentation, + }) => + MethodMemberGroup( + name: name, + isNullable: false, + isStatic: false, + modifier: Modifier.unset, + annotation: Annotation.unset, + memberType: memberType, + rawRepresentation: rawRepresentation, + ); + + /// Parses [MethodDeclaration] and returns instance of [MethodMemberGroup] + factory MethodMemberGroup.parse(MethodDeclaration declaration) { + final methodName = declaration.name.lexeme; + final annotation = parseAnnotation(declaration); + final modifier = Identifier.isPrivateName(methodName) + ? Modifier.private + : Modifier.public; + + return MethodMemberGroup( + name: methodName.toLowerCase(), + annotation: annotation ?? Annotation.unset, + isStatic: declaration.isStatic, + isNullable: declaration.returnType?.question != null, + memberType: MemberType.method, + modifier: modifier, + rawRepresentation: '', + ); + } + + @override + int getSortingCoefficient() { + var coefficient = 0; + + coefficient += isStatic ? 1 : 0; + coefficient += isNullable ? 1 : 0; + coefficient += annotation != Annotation.unset ? 1 : 0; + coefficient += modifier != Modifier.unset ? 1 : 0; + coefficient += name != null ? 10 : 0; + + return coefficient; + } + + @override + String toString() => rawRepresentation; +} diff --git a/lib/lints/member_ordering/models/member_ordering_parameters.dart b/lib/lints/member_ordering/models/member_ordering_parameters.dart new file mode 100644 index 00000000..44b36bb4 --- /dev/null +++ b/lib/lints/member_ordering/models/member_ordering_parameters.dart @@ -0,0 +1,66 @@ +// 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:solid_lints/lints/member_ordering/config_parser.dart'; +import 'package:solid_lints/lints/member_ordering/models/member_group/member_group.dart'; + +/// A data model class that represents the member ordering input +/// parameters. +class MemberOrderingParameters { + /// Config keys + static const _orderConfig = 'order'; + static const _widgetsOrderConfig = 'widgets-order'; + static const _alphabetizeConfig = 'alphabetize'; + static const _alphabetizeByTypeConfig = 'alphabetize-by-type'; + + /// Config used for members of regular class + final List groupsOrder; + + /// Config used for members of widget class + final List widgetsGroupsOrder; + + /// Indicates if params should be in alphabetical order + final bool alphabetize; + + /// Indicates if params should be in alphabetical order of their type + final bool alphabetizeByType; + + /// Constructor for [MemberOrderingParameters] model + const MemberOrderingParameters({ + required this.groupsOrder, + required this.widgetsGroupsOrder, + required this.alphabetize, + required this.alphabetizeByType, + }); + + /// Method for creating from json data + factory MemberOrderingParameters.fromJson(Map json) => + MemberOrderingParameters( + groupsOrder: MemberOrderingConfigParser.parseOrder(json[_orderConfig]), + widgetsGroupsOrder: MemberOrderingConfigParser.parseWidgetsOrder( + json[_widgetsOrderConfig], + ), + alphabetize: json[_alphabetizeConfig] as bool? ?? false, + alphabetizeByType: json[_alphabetizeByTypeConfig] as bool? ?? false, + ); +} diff --git a/lib/lints/member_ordering/models/member_type.dart b/lib/lints/member_ordering/models/member_type.dart new file mode 100644 index 00000000..e978dd7e --- /dev/null +++ b/lib/lints/member_ordering/models/member_type.dart @@ -0,0 +1,57 @@ +// 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'; + +/// A data model enum represents class member type affiliation +enum MemberType { + /// Indicates fields affiliation + field('fields'), + + /// Indicates method affiliation + method('methods', typeAlias: 'method'), + + /// Indicates constructor affiliation + constructor('constructors'), + + /// Indicates getters affiliation + getter('getters'), + + /// Indicates setters affiliation + setter('setters'), + + /// Indicates affiliation with both getters and setters + getterAndSetter('getters-setters'); + + /// String representation of member group type + final String type; + + /// Alternative string representation of member group type + final String? typeAlias; + + const MemberType(this.type, {this.typeAlias}); + + /// Parses a String member type and returns instance of [MemberType] + static MemberType? parse(String? name) => values + .firstWhereOrNull((type) => name == type.type || name == type.typeAlias); +} diff --git a/lib/lints/member_ordering/models/modifier.dart b/lib/lints/member_ordering/models/modifier.dart new file mode 100644 index 00000000..b47b7f89 --- /dev/null +++ b/lib/lints/member_ordering/models/modifier.dart @@ -0,0 +1,44 @@ +// 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. + +/// A data model enum represents class member access modifier +enum Modifier { + /// Indicates public access modifier + public('public'), + + /// Indicates private access modifier + private('private'), + + /// Indicates missing access modifier + /// used to handle cases of unsupported field type keywords + unset('unset'); + + /// String representation of access modifier keyword + final String type; + + const Modifier(this.type); + + /// Parses a String access modifier and returns instance of [Modifier] + static Modifier parse(String? name) => values + .firstWhere((type) => type.type == name, orElse: () => Modifier.unset); +} diff --git a/lib/solid_lints.dart b/lib/solid_lints.dart index eb09549c..7c05fee6 100644 --- a/lib/solid_lints.dart +++ b/lib/solid_lints.dart @@ -12,6 +12,7 @@ import 'package:solid_lints/lints/avoid_unrelated_type_assertions/avoid_unrelate import 'package:solid_lints/lints/cyclomatic_complexity/cyclomatic_complexity_metric.dart'; import 'package:solid_lints/lints/double_literal_format/double_literal_format_rule.dart'; import 'package:solid_lints/lints/function_lines_of_code/function_lines_of_code_metric.dart'; +import 'package:solid_lints/lints/member_ordering/member_ordering_rule.dart'; import 'package:solid_lints/lints/newline_before_return/newline_before_return_rule.dart'; import 'package:solid_lints/lints/no_empty_block/no_empty_block_rule.dart'; import 'package:solid_lints/lints/no_equal_then_else/no_equal_then_else_rule.dart'; @@ -41,6 +42,7 @@ class _SolidLints extends PluginBase { NewlineBeforeReturnRule.createRule(configs), NoEmptyBlockRule.createRule(configs), NoEqualThenElseRule.createRule(configs), + MemberOrderingRule.createRule(configs), ]; // Return only enabled rules diff --git a/lint_test/alphabetize_by_type_test/alphabetize_by_type_test.dart b/lint_test/alphabetize_by_type_test/alphabetize_by_type_test.dart new file mode 100644 index 00000000..5f8a8643 --- /dev/null +++ b/lint_test/alphabetize_by_type_test/alphabetize_by_type_test.dart @@ -0,0 +1,25 @@ +// ignore_for_file: unused_field +// ignore_for_file: unused_element + +/// Check the `member-ordering` rule +/// alphabetical-by-type option enabled + +class CorrectAlphabeticalByTypeClass { + final double e = 1; + final int a = 1; +} + +class WrongAlphabeticalByTypeClass { + final int a = 1; + + // expect_lint: member-ordering + final double e = 1; +} + +class PartiallyWrongAlphabeticalByTypeClass { + final int a = 1; + final String str = 's'; + + // expect_lint: member-ordering + final double e = 1; +} diff --git a/lint_test/alphabetize_by_type_test/analysis_options.yaml b/lint_test/alphabetize_by_type_test/analysis_options.yaml new file mode 100644 index 00000000..b77c8f7a --- /dev/null +++ b/lint_test/alphabetize_by_type_test/analysis_options.yaml @@ -0,0 +1,8 @@ +analyzer: + plugins: + - ../custom_lint + +custom_lint: + rules: + - member-ordering: + alphabetize-by-type: true diff --git a/lint_test/alphabetize_by_type_test/pubspec.yaml b/lint_test/alphabetize_by_type_test/pubspec.yaml new file mode 100644 index 00000000..fb785ca7 --- /dev/null +++ b/lint_test/alphabetize_by_type_test/pubspec.yaml @@ -0,0 +1,14 @@ +name: solid_lints_alphabetize_by_type_test +publish_to: none + +environment: + sdk: '>=3.0.0 <4.0.0' + +dependencies: + flutter: + sdk: flutter + +dev_dependencies: + solid_lints: + path: ../../ + test: ^1.20.1 diff --git a/lint_test/analysis_options.yaml b/lint_test/analysis_options.yaml index 89028572..7704fc95 100644 --- a/lint_test/analysis_options.yaml +++ b/lint_test/analysis_options.yaml @@ -22,3 +22,28 @@ custom_lint: - newline-before-return - no-empty-block - no-equal-then-else + - member-ordering: + alphabetize: true + order: + - public-fields + - private-fields + - constructors + - getters + - setters + - public-methods + - private-methods + - close-method + widgets-order: + - const-fields + - static-fields + - static-methods + - public-fields + - private-fields + - public-methods + - private-methods + - constructors + - build-method + - init-state-method + - did-change-dependencies-method + - did-update-widget-method + - dispose-method diff --git a/lint_test/avoid_non_null_assertion_test.dart b/lint_test/avoid_non_null_assertion_test.dart index c41c35ba..58c94c03 100644 --- a/lint_test/avoid_non_null_assertion_test.dart +++ b/lint_test/avoid_non_null_assertion_test.dart @@ -1,4 +1,5 @@ // ignore_for_file: avoid_global_state +// ignore_for_file: member-ordering /// Check "bang" operator fail /// diff --git a/lint_test/avoid_returning_widget_test.dart b/lint_test/avoid_returning_widget_test.dart index 48d414d3..76099921 100644 --- a/lint_test/avoid_returning_widget_test.dart +++ b/lint_test/avoid_returning_widget_test.dart @@ -1,4 +1,5 @@ // ignore_for_file: unused_element +// ignore_for_file: member-ordering /// Check returning a widget fail /// `avoid_returning_widgets` diff --git a/lint_test/avoid_unnecessary_setstate_test.dart b/lint_test/avoid_unnecessary_setstate_test.dart index 2d550dfe..79dbdb13 100644 --- a/lint_test/avoid_unnecessary_setstate_test.dart +++ b/lint_test/avoid_unnecessary_setstate_test.dart @@ -21,6 +21,8 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. +// ignore_for_file: member-ordering + import 'package:flutter/material.dart'; /// Check unnecessary setstate fail diff --git a/lint_test/member_ordering_test.dart b/lint_test/member_ordering_test.dart new file mode 100644 index 00000000..45ecd931 --- /dev/null +++ b/lint_test/member_ordering_test.dart @@ -0,0 +1,232 @@ +// ignore_for_file: unused_field +// ignore_for_file: unused_element +// ignore_for_file: no-empty-block + +import 'package:flutter/widgets.dart'; + +/// Check the `member-ordering` rule + +class AlphabeticalClass { + final b = 1; + + // expect_lint: member-ordering + final a = 1; + final c = 1; + + void bStuff() {} + + // expect_lint: member-ordering + void aStuff() {} + + void cStuff() {} + + void visitStatement() {} + + // expect_lint: member-ordering + void visitStanford() {} +} + +class CorrectOrder { + final publicField = 1; + int _privateField = 2; + + CorrectOrder(); + + int get privateFieldGetter => _privateField; + + void set privateFieldSetter(int value) { + _privateField = value; + } + + void publicDoStuff() {} + + void _privateDoStuff() {} + + void close() {} +} + +class WrongOrder { + void close() {} + + // expect_lint: member-ordering + void _privateDoStuff() {} + + // expect_lint: member-ordering + void publicDoStuff() {} + + // expect_lint: member-ordering + void set privateFieldSetter(int value) { + _privateField = value; + } + + // expect_lint: member-ordering + int get privateFieldGetter => _privateField; + + // expect_lint: member-ordering + WrongOrder(); + + // expect_lint: member-ordering + int _privateField = 2; + + // expect_lint: member-ordering + final publicField = 1; +} + +class PartiallyWrongOrder { + final publicField = 1; + + PartiallyWrongOrder(); + + // expect_lint: member-ordering + int _privateField = 2; + + int get privateFieldGetter => _privateField; + + void set privateFieldSetter(int value) { + _privateField = value; + } + + void _privateDoStuff() {} + + // expect_lint: member-ordering + void publicDoStuff() {} + + void close() {} +} + +class CorrectWidget extends StatefulWidget { + @override + State createState() => _CorrectWidgetState(); + + const CorrectWidget({super.key}); +} + +class _CorrectWidgetState extends State { + static const constField = 1; + static final staticField = 1; + + static void staticDoStuff() {} + + final publicField = 1; + final _privateField = 1; + + void publicDoStuff() {} + + void _privateDoStuff() {} + + _CorrectWidgetState(); + + @override + Widget build(BuildContext context) => throw UnimplementedError(); + + @override + void initState() => super.initState(); + + @override + void didChangeDependencies() => super.didChangeDependencies(); + + @override + void didUpdateWidget(covariant CorrectWidget oldWidget) => + super.didUpdateWidget(oldWidget); + + @override + void dispose() => super.dispose(); +} + +class WrongWidget extends StatefulWidget { + const WrongWidget({super.key}); + + // expect_lint: member-ordering + @override + State createState() => _WrongWidgetState(); +} + +class _WrongWidgetState extends State { + @override + void dispose() => super.dispose(); + + // expect_lint: member-ordering + @override + void didUpdateWidget(covariant WrongWidget oldWidget) => + super.didUpdateWidget(oldWidget); + + // expect_lint: member-ordering + @override + void didChangeDependencies() => super.didChangeDependencies(); + + // expect_lint: member-ordering + @override + void initState() => super.initState(); + + // expect_lint: member-ordering + @override + Widget build(BuildContext context) => throw UnimplementedError(); + + // expect_lint: member-ordering + _WrongWidgetState(); + + // expect_lint: member-ordering + void _privateDoStuff() {} + + // expect_lint: member-ordering + void publicDoStuff() {} + + // expect_lint: member-ordering + final _privateField = 1; + + // expect_lint: member-ordering + final publicField = 1; + + // expect_lint: member-ordering + static void staticDoStuff() {} + + // expect_lint: member-ordering + static final staticField = 1; + + // expect_lint: member-ordering + static const constField = 1; +} + +class PartiallyCorrectWidget extends StatefulWidget { + @override + State createState() => _PartiallyCorrectWidgetState(); + + const PartiallyCorrectWidget({super.key}); +} + +class _PartiallyCorrectWidgetState extends State { + static final staticField = 1; + + // expect_lint: member-ordering + static const constField = 1; + + static void staticDoStuff() {} + + final _privateField = 1; + + // expect_lint: member-ordering + final publicField = 1; + + void publicDoStuff() {} + + void _privateDoStuff() {} + + _PartiallyCorrectWidgetState(); + + @override + Widget build(BuildContext context) => throw UnimplementedError(); + + @override + void didChangeDependencies() => super.didChangeDependencies(); + + // expect_lint: member-ordering + @override + void initState() => super.initState(); + + @override + void didUpdateWidget(covariant PartiallyCorrectWidget oldWidget) => + super.didUpdateWidget(oldWidget); + + @override + void dispose() => super.dispose(); +} diff --git a/lint_test/newline_before_return_test.dart b/lint_test/newline_before_return_test.dart index 877e5365..3e061583 100644 --- a/lint_test/newline_before_return_test.dart +++ b/lint_test/newline_before_return_test.dart @@ -1,4 +1,5 @@ // ignore_for_file: unused_local_variable +// ignore_for_file: member-ordering /// Check the `newline-before-return` rule class Foo {