Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add prefer first or null rule #150

Merged
merged 3 commits into from
Nov 6, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
1 change: 1 addition & 0 deletions example/analysis_options.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ dart_code_linter:
- avoid-unnecessary-type-casts
- avoid-unused-parameters
- newline-before-return
- prefer-first-or-null
- no-blank-line-before-single-return
- no-boolean-literal-compare
- no-empty-block
Expand Down
2 changes: 2 additions & 0 deletions lib/src/analyzers/lint_analyzer/rules/rules_factory.dart
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ import 'rules_list/prefer_define_hero_tag/prefer_define_hero_tag_rule.dart';
import 'rules_list/prefer_enums_by_name/prefer_enums_by_name_rule.dart';
import 'rules_list/prefer_extracting_callbacks/prefer_extracting_callbacks_rule.dart';
import 'rules_list/prefer_first/prefer_first_rule.dart';
import 'rules_list/prefer_first_or_null/prefer_first_or_null_rule.dart';
import 'rules_list/prefer_immediate_return/prefer_immediate_return_rule.dart';
import 'rules_list/prefer_intl_name/prefer_intl_name_rule.dart';
import 'rules_list/prefer_iterable_of/prefer_iterable_of_rule.dart';
Expand Down Expand Up @@ -132,6 +133,7 @@ final _implementedRules = <String, Rule Function(Map<String, Object>)>{
MissingTestAssertionRule.ruleId: MissingTestAssertionRule.new,
NewlineBeforeReturnRule.ruleId: NewlineBeforeReturnRule.new,
NoBlankLineBeforeSingleReturnRule.ruleId: NoBlankLineBeforeSingleReturnRule.new,
PreferFirstOrNullRule.ruleId: PreferFirstOrNullRule.new,
NoBooleanLiteralCompareRule.ruleId: NoBooleanLiteralCompareRule.new,
NoEmptyBlockRule.ruleId: NoEmptyBlockRule.new,
NoEqualArgumentsRule.ruleId: NoEqualArgumentsRule.new,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// ignore_for_file: public_member_api_docs

import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/ast/visitor.dart';
import 'package:analyzer/dart/element/type.dart';

import '../../../../../utils/dart_types_utils.dart';
import '../../../../../utils/node_utils.dart';
import '../../../lint_utils.dart';
import '../../../models/internal_resolved_unit_result.dart';
import '../../../models/issue.dart';
import '../../../models/replacement.dart';
import '../../../models/severity.dart';
import '../../models/dart_rule.dart';
import '../../rule_utils.dart';

part 'visitor.dart';

class PreferFirstOrNullRule extends DartRule {
static const ruleId = 'prefer-first-or-null';
static const warningMessage = 'Use first instead of accessing the element at zero index or using first.';
santitigaga marked this conversation as resolved.
Show resolved Hide resolved
static const replaceComment = "Replace with 'firstOrNull'.";

PreferFirstOrNullRule([Map<String, Object> config = const {}])
: super(
id: ruleId,
severity: readSeverity(config, Severity.style),
excludes: readExcludes(config),
includes: readIncludes(config),
);

@override
Iterable<Issue> check(InternalResolvedUnitResult source) {
final visitor = _Visitor();
source.unit.visitChildren(visitor);

return visitor.expressions
.map((expression) => createIssue(
rule: this,
location: nodeLocation(node: expression, source: source),
message: warningMessage,
replacement: _createReplacement(expression),
))
.toList(growable: false);
}

Replacement _createReplacement(Expression expression) {
String replacement;

if (expression is MethodInvocation) {
replacement = expression.isCascaded ? '..firstOrNull' : '${expression.target ?? ''}.firstOrNull';
} else if (expression is IndexExpression) {
replacement = expression.isCascaded ? '..firstOrNull' : '${expression.target ?? ''}.firstOrNull';
} else if (expression is PrefixedIdentifier) {
replacement = '${expression.prefix.token.lexeme}.firstOrNull';
} else {
replacement = '.firstOrNull';
}

return Replacement(
comment: replaceComment,
replacement: replacement,
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
part of 'prefer_first_or_null_rule.dart';

class _Visitor extends RecursiveAstVisitor<void> {
final _expressions = <Expression>[];

Iterable<Expression> get expressions => _expressions;

@override
void visitPrefixedIdentifier(PrefixedIdentifier node) {
super.visitPrefixedIdentifier(node);

final targetType = node.prefix.staticType;

if (node.identifier.name == 'first' && targetType is InterfaceType && isIterableOrSubclass(targetType)) {
_expressions.add(node);
}
}

@override
void visitMethodInvocation(MethodInvocation node) {
super.visitMethodInvocation(node);

if (isIterableOrSubclass(node.realTarget?.staticType)) {
if (node.methodName.name == 'elementAt') {
final arg = node.argumentList.arguments.first;

if (arg is IntegerLiteral && arg.value == 0) {
_expressions.add(node);
}
} else if (node.methodName.name == 'first') {
_expressions.add(node);
}
}
}

@override
void visitIndexExpression(IndexExpression node) {
super.visitIndexExpression(node);

if (isListOrSubclass(node.realTarget.staticType)) {
final index = node.index;

if (index is IntegerLiteral && index.value == 0) {
_expressions.add(node);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ void main() {
final unit = await RuleTestHelper.resolveFromFile(_examplePath);
final issues = NoBlankLineBeforeSingleReturnRule().check(unit);

List<int> startLines = [79, 85, 92, 100, 106, 113, 122];
final startLines = <int>[79, 85, 92, 100, 106, 113, 122];

RuleTestHelper.verifyIssues(
issues: issues,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
// ignore_for_file: unnecessary_cast, unused_local_variable
santitigaga marked this conversation as resolved.
Show resolved Hide resolved

import 'dart:collection';

const _array = [1, 2, 3, 4, 5, 6, 7, 8, 9];

void func() {
const iterable = _array as Iterable<int>;

final firstOfIterable = iterable.first; // LINT
final firstElementOfIterable = iterable.elementAt(0); // LINT
final secondElementOfIterable = iterable.elementAt(1);

iterable
..elementAt(2)
..elementAt(0) // LINT
..elementAt(1);

////////////////////////////////////////////////////////////////////////////////

const list = _array;

final firstOfList = list.first; // LINT
final firstElementOfList1 = list.elementAt(0); // LINT
final firstElementOfList2 = list[0]; // LINT
final secondElementOfList1 = list.elementAt(1);
final secondElementOfList2 = list[1];

list
..elementAt(2)
..elementAt(0) // LINT
..elementAt(1)
..[2]
..[0] // LINT
..[1];

(list
..[2]
..[0]) // LINT
.length;

list
..[2].toDouble()
..[0].toDouble(); // LINT

////////////////////////////////////////////////////////////////////////////////

final set = _array.toSet();

final firstOfSet = set.first; // LINT
final firstElementOfSet = set.elementAt(0); // LINT
final secondElementOfSet = set.elementAt(1);

set
..elementAt(2)
..elementAt(0) // LINT
..elementAt(1);

////////////////////////////////////////////////////////////////////////////////

final doubleLinkedQueue = DoubleLinkedQueue.of(_array);

final firstOfDoubleLinkedQueue = doubleLinkedQueue.first; // LINT
final firstElementOfDoubleLinkedQueue =
doubleLinkedQueue.elementAt(0); // LINT
final secondElementOfDoubleLinkedQueue = doubleLinkedQueue.elementAt(1);

doubleLinkedQueue
..elementAt(2)
..elementAt(0) // LINT
..elementAt(1);

////////////////////////////////////////////////////////////////////////////////

final hashSet = HashSet.of(_array);

final firstOfHashSet = hashSet.first; // LINT
final firstElementOfHashSet = hashSet.elementAt(0); // LINT
final secondElementOfHashSet = hashSet.elementAt(1);

hashSet
..elementAt(2)
..elementAt(0) // LINT
..elementAt(1);

////////////////////////////////////////////////////////////////////////////////

final linkedHashSet = LinkedHashSet.of(_array);

final firstOfLinkedHashSet = linkedHashSet.first; // LINT
final firstElementOfLinkedHashSet = linkedHashSet.elementAt(0); // LINT
final secondElementOfLinkedHashSet = linkedHashSet.elementAt(1);

linkedHashSet
..elementAt(2)
..elementAt(0) // LINT
..elementAt(1);

////////////////////////////////////////////////////////////////////////////////

final listQueue = ListQueue.of(_array);

final firstOfListQueue = listQueue.first; // LINT
final firstElementOfListQueue = listQueue.elementAt(0); // LINT
final secondElementOfListQueue = listQueue.elementAt(1);

listQueue
..elementAt(2)
..elementAt(0) // LINT
..elementAt(1);

////////////////////////////////////////////////////////////////////////////////

final queue = Queue.of(_array);

final firstOfQueue = queue.first; // LINT
final firstElementOfQueue = queue.elementAt(0); // LINT
final secondElementOfQueue = queue.elementAt(1);

queue
..elementAt(2)
..elementAt(0) // LINT
..elementAt(1);

////////////////////////////////////////////////////////////////////////////////

final splayTreeSet = SplayTreeSet.of(_array);

final firstOfSplayTreeSet = splayTreeSet.first; // LINT
final firstElementOfSplayTreeSet = splayTreeSet.elementAt(0); // LINT
final secondElementOfSplayTreeSet = splayTreeSet.elementAt(1);

splayTreeSet
..elementAt(2)
..elementAt(0) // LINT
..elementAt(1);

////////////////////////////////////////////////////////////////////////////////

final unmodifiableListView = UnmodifiableListView<int>(_array);

final firstOfUnmodifiableListView = unmodifiableListView.first; // LINT
final firstElementOfUnmodifiableListView1 =
unmodifiableListView.elementAt(0); // LINT
final firstElementOfUnmodifiableListView2 = unmodifiableListView[0]; // LINT
final secondElementOfUnmodifiableListView1 =
unmodifiableListView.elementAt(1);
final secondElementOfUnmodifiableListView2 = unmodifiableListView[1];

unmodifiableListView
..elementAt(2)
..elementAt(0) // LINT
..elementAt(1)
..[2]
..[0] // LINT
..[1];

////////////////////////////////////////////////////////////////////////////////

final unmodifiableSetView = UnmodifiableSetView<int>(_array.toSet());

final firstOfUnmodifiableSetView = unmodifiableSetView.first; // LINT
final firstElementOfUnmodifiableSetView =
unmodifiableSetView.elementAt(0); // LINT
final secondElementOfUnmodifiableSetView = unmodifiableSetView.elementAt(1);

unmodifiableSetView
..elementAt(2)
..elementAt(0) // LINT
..elementAt(1);
}
Loading