Skip to content
This repository was archived by the owner on Jul 16, 2023. It is now read-only.

feat: add prefer-define-hero-tag rule #1184

Merged
merged 5 commits into from
Mar 17, 2023
Merged
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

## Unreleased

* feat: add static code diagnostic [prefer-define-hero-tag](https://github.com/dart-code-checker/dart-code-metrics/issues/1027).
* chore: restrict `analyzer` version to `>=5.1.0 <5.8.0`.

## 5.6.0
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 @@ -59,6 +59,7 @@ import 'rules_list/prefer_correct_edge_insets_constructor/prefer_correct_edge_in
import 'rules_list/prefer_correct_identifier_length/prefer_correct_identifier_length_rule.dart';
import 'rules_list/prefer_correct_test_file_name/prefer_correct_test_file_name_rule.dart';
import 'rules_list/prefer_correct_type_name/prefer_correct_type_name_rule.dart';
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';
Expand Down Expand Up @@ -149,6 +150,7 @@ final _implementedRules = <String, Rule Function(Map<String, Object>)>{
PreferCorrectIdentifierLengthRule.new,
PreferCorrectTestFileNameRule.ruleId: PreferCorrectTestFileNameRule.new,
PreferCorrectTypeNameRule.ruleId: PreferCorrectTypeNameRule.new,
PreferDefineHeroTagRule.ruleId: PreferDefineHeroTagRule.new,
PreferEnumsByNameRule.ruleId: PreferEnumsByNameRule.new,
PreferExtractingCallbacksRule.ruleId: PreferExtractingCallbacksRule.new,
PreferFirstRule.ruleId: PreferFirstRule.new,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// ignore_for_file: public_member_api_docs

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

import '../../../../../utils/node_utils.dart';
import '../../../lint_utils.dart';
import '../../../models/internal_resolved_unit_result.dart';
import '../../../models/issue.dart';
import '../../../models/severity.dart';
import '../../models/flutter_rule.dart';
import '../../rule_utils.dart';

part 'visitor.dart';

class PreferDefineHeroTagRule extends FlutterRule {
static const ruleId = 'prefer-define-hero-tag';
static const _issueMessage = 'Prefer define heroTag property.';

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

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

return visitor.invocations
.map((invocation) => createIssue(
rule: this,
location: nodeLocation(node: invocation, source: source),
message: _issueMessage,
))
.toList(growable: false);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
part of 'prefer_define_hero_tag_rule.dart';

const _cupertinoNavigationBarClassName = 'CupertinoNavigationBar';
const _cupertinoSliverNavigationBarClassName = 'CupertinoSliverNavigationBar';
const _floatingActionButtonClassName = 'FloatingActionButton';
const _constructorExtendedName = 'extended';
const _constructorLargeName = 'large';
const _constructorSmallName = 'small';
const _heroTagPropertyName = 'heroTag';

class _Visitor extends RecursiveAstVisitor<void> {
final _invocations = <MethodInvocation>[];

Iterable<MethodInvocation> get invocations => _invocations;

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

final methodName = node.methodName.name;
if (methodName == _cupertinoNavigationBarClassName ||
methodName == _cupertinoSliverNavigationBarClassName ||
methodName == _floatingActionButtonClassName ||
node.beginToken.lexeme == _floatingActionButtonClassName &&
(methodName == _constructorExtendedName ||
methodName == _constructorLargeName ||
methodName == _constructorSmallName)) {
if (!node.argumentList.arguments.any((arg) =>
arg is NamedExpression &&
arg.name.label.name == _heroTagPropertyName)) {
_invocations.add(node);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
class MyWidget extends StatelessWidget {
const MyWidget({super.key});

@override
Widget build(BuildContext context) => Scaffold(
floatingActionButton: FloatingActionButton(
onPressed: () {},
),
);
}

class MyWidget2 extends StatelessWidget {
const MyWidget2({super.key});

@override
Widget build(BuildContext context) => Scaffold(
floatingActionButton: FloatingActionButton.extended(
label: const Text('label'),
onPressed: () {},
),
);
}

class MyWidget3 extends StatelessWidget {
const MyWidget3({super.key});

@override
Widget build(BuildContext context) => Scaffold(
floatingActionButton: FloatingActionButton.large(
onPressed: () {},
),
);
}

class MyWidget4 extends StatelessWidget {
const MyWidget4({super.key});

@override
Widget build(BuildContext context) => Scaffold(
floatingActionButton: FloatingActionButton.small(
onPressed: () {},
),
);
}

class MyWidget6 extends StatelessWidget {
const MyWidget6({super.key});

@override
Widget build(BuildContext context) => Scaffold(
floatingActionButton: FloatingActionButton(
heroTag: 'heroTag',
onPressed: () {},
),
);
}

class MyWidget7 extends StatelessWidget {
const MyWidget7({super.key});

@override
Widget build(BuildContext context) => Scaffold(
floatingActionButton: FloatingActionButton.extended(
heroTag: 'heroTag',
label: const Text('label'),
onPressed: () {},
),
);
}

class MyWidget8 extends StatelessWidget {
const MyWidget8({super.key});

@override
Widget build(BuildContext context) => Scaffold(
floatingActionButton: FloatingActionButton.large(
heroTag: 'heroTag',
onPressed: () {},
),
);
}

class MyWidget9 extends StatelessWidget {
const MyWidget9({super.key});

@override
Widget build(BuildContext context) => Scaffold(
floatingActionButton: FloatingActionButton.small(
heroTag: 'heroTag',
onPressed: () {},
),
);
}

class SliverNavBarExample extends StatelessWidget {
const SliverNavBarExample({super.key});

@override
Widget build(BuildContext context) => const CupertinoPageScaffold(
child: CustomScrollView(
slivers: <Widget>[
CupertinoSliverNavigationBar(
largeTitle: Text('Contacts'),
),
],
),
);
}

class SliverNavBarExample2 extends StatelessWidget {
const SliverNavBarExample2({super.key});

@override
Widget build(BuildContext context) => const CupertinoPageScaffold(
child: CustomScrollView(
slivers: <Widget>[
CupertinoSliverNavigationBar(
largeTitle: Text('Contacts'),
heroTag: 'heroTag',
),
],
),
);
}

class NavBarExample extends StatelessWidget {
const NavBarExample({super.key});

@override
Widget build(BuildContext context) => const CupertinoPageScaffold(
navigationBar: CupertinoNavigationBar(
middle: Text('CupertinoNavigationBar Sample'),
),
child: Text('data'),
);
}

class NavBarExample2 extends StatelessWidget {
const NavBarExample2({super.key});

@override
Widget build(BuildContext context) => const CupertinoPageScaffold(
navigationBar: CupertinoNavigationBar(
heroTag: 'heroTag',
middle: Text('CupertinoNavigationBar Sample'),
),
child: Text('data'),
);
}

class BuildContext {}

class Key {}

class Widget {
final Key? key;

const Widget(this.key);
}

class StatelessWidget extends Widget {
const StatelessWidget(super.key);
}

class Scaffold extends Widget {
final Widget? floatingActionButton;

Scaffold({
this.floatingActionButton,
});
}

class CupertinoPageScaffold extends Widget {
final Widget child;
final Widget? navigationBar;

const CupertinoPageScaffold({
super.key,
required this.child,
this.navigationBar,
});
}

class CustomScrollView extends Widget {
final List<Widget> slivers;

const CustomScrollView({
super.key,
required this.slivers,
});
}

class Text extends Widget {
final String data;

const Text(
this.data, {
super.key,
});
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import 'package:dart_code_metrics/src/analyzers/lint_analyzer/models/severity.dart';
import 'package:dart_code_metrics/src/analyzers/lint_analyzer/rules/rules_list/prefer_define_hero_tag/prefer_define_hero_tag_rule.dart';
import 'package:test/test.dart';

import '../../../../../helpers/rule_test_helper.dart';

const _examplePath = 'prefer_define_hero_tag/examples/example.dart';

void main() {
group(
'PreferDefineHeroTagRule',
() {
test('initialization', () async {
final unit = await RuleTestHelper.resolveFromFile(_examplePath);
final issues = PreferDefineHeroTagRule().check(unit);

RuleTestHelper.verifyInitialization(
issues: issues,
ruleId: 'prefer-define-hero-tag',
severity: Severity.warning,
);
});

test('reports about found issues', () async {
final unit = await RuleTestHelper.resolveFromFile(_examplePath);
final issues = PreferDefineHeroTagRule().check(unit);

RuleTestHelper.verifyIssues(
issues: issues,
startLines: [6, 17, 29, 40, 102, 131],
startColumns: [31, 31, 31, 31, 13, 24],
messages: [
'Prefer define heroTag property.',
'Prefer define heroTag property.',
'Prefer define heroTag property.',
'Prefer define heroTag property.',
'Prefer define heroTag property.',
'Prefer define heroTag property.',
],
locationTexts: [
'''
FloatingActionButton(
onPressed: () {},
)''',
'''
FloatingActionButton.extended(
label: const Text('label'),
onPressed: () {},
)''',
'''
FloatingActionButton.large(
onPressed: () {},
)''',
'''
FloatingActionButton.small(
onPressed: () {},
)''',
'''
CupertinoSliverNavigationBar(
largeTitle: Text('Contacts'),
)''',
'''
CupertinoNavigationBar(
middle: Text('CupertinoNavigationBar Sample'),
)''',
],
);
});
},
);
}