new lint rule: use_truncating_division
Fixes https://github.com/dart-lang/linter/issues/3930
This allows us to remove (softly) the DIVISION_OPTIMIZATION HintCode,
which was not deemed a good candidate for a Warning.
Cq-Include-Trybots: luci.dart.try:flutter-analyze-try,analyzer-win-release-try,pkg-win-release-try
Change-Id: Iac48c94687d0b6b32c971e9f366ccb96adf34429
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/378543
Reviewed-by: Phil Quitslund <pquitslund@google.com>
Commit-Queue: Samuel Rawlins <srawlins@google.com>
Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
diff --git a/pkg/analysis_server/lib/src/services/correction/error_fix_status.yaml b/pkg/analysis_server/lib/src/services/correction/error_fix_status.yaml
index 02ec449..75d0d24 100644
--- a/pkg/analysis_server/lib/src/services/correction/error_fix_status.yaml
+++ b/pkg/analysis_server/lib/src/services/correction/error_fix_status.yaml
@@ -2485,6 +2485,8 @@
status: noFix
notes: |-
This would require renaming the method, which is a refactoring.
+LintCode.use_truncating_division:
+ status: hasFix
LintCode.valid_regexps:
status: noFix
LintCode.void_checks:
diff --git a/pkg/analysis_server/lib/src/services/correction/fix_internal.dart b/pkg/analysis_server/lib/src/services/correction/fix_internal.dart
index 9e25b7c..1184fae 100644
--- a/pkg/analysis_server/lib/src/services/correction/fix_internal.dart
+++ b/pkg/analysis_server/lib/src/services/correction/fix_internal.dart
@@ -383,6 +383,7 @@
import 'package:linter/src/rules/use_rethrow_when_possible.dart';
import 'package:linter/src/rules/use_string_in_part_of_directives.dart';
import 'package:linter/src/rules/use_super_parameters.dart';
+import 'package:linter/src/rules/use_truncating_division.dart';
final _builtInLintMultiProducers = {
CommentReferences.code: [
@@ -859,6 +860,9 @@
UseSuperParameters.multipleParams: [
ConvertToSuperParameters.new,
],
+ UseTruncatingDivision.code: [
+ UseEffectiveIntegerDivision.new,
+ ],
};
final _builtInNonLintMultiProducers = {
diff --git a/pkg/analyzer/lib/src/error/best_practices_verifier.dart b/pkg/analyzer/lib/src/error/best_practices_verifier.dart
index c97b253..738b9cd 100644
--- a/pkg/analyzer/lib/src/error/best_practices_verifier.dart
+++ b/pkg/analyzer/lib/src/error/best_practices_verifier.dart
@@ -852,6 +852,8 @@
/// Checks the passed binary expression for [HintCode.DIVISION_OPTIMIZATION].
///
/// Returns whether a hint code is generated.
+ // TODO(srawlins): Remove this ASAP, as it is being replaced by the
+ // 'use_truncating_division' lint rule, to avoid double reporting.
bool _checkForDivisionOptimizationHint(BinaryExpression node) {
if (node.operator.type != TokenType.SLASH) return false;
diff --git a/pkg/linter/example/all.yaml b/pkg/linter/example/all.yaml
index 7b66889..aaa5e4d 100644
--- a/pkg/linter/example/all.yaml
+++ b/pkg/linter/example/all.yaml
@@ -223,5 +223,6 @@
- use_super_parameters
- use_test_throws_matchers
- use_to_and_as_if_applicable
+ - use_truncating_division
- valid_regexps
- void_checks
diff --git a/pkg/linter/lib/src/rules.dart b/pkg/linter/lib/src/rules.dart
index 0e73c6f..9a0f5f9 100644
--- a/pkg/linter/lib/src/rules.dart
+++ b/pkg/linter/lib/src/rules.dart
@@ -237,6 +237,7 @@
import 'rules/use_super_parameters.dart';
import 'rules/use_test_throws_matchers.dart';
import 'rules/use_to_and_as_if_applicable.dart';
+import 'rules/use_truncating_division.dart';
import 'rules/valid_regexps.dart';
import 'rules/void_checks.dart';
@@ -476,6 +477,7 @@
..register(UseSuperParameters())
..register(UseTestThrowsMatchers())
..register(UseToAndAsIfApplicable())
+ ..register(UseTruncatingDivision())
..register(ValidRegexps())
..register(VoidChecks());
}
diff --git a/pkg/linter/lib/src/rules/use_truncating_division.dart b/pkg/linter/lib/src/rules/use_truncating_division.dart
new file mode 100644
index 0000000..10bfb7a
--- /dev/null
+++ b/pkg/linter/lib/src/rules/use_truncating_division.dart
@@ -0,0 +1,97 @@
+// Copyright (c) 2024, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:analyzer/dart/ast/ast.dart';
+import 'package:analyzer/dart/ast/token.dart';
+import 'package:analyzer/dart/ast/visitor.dart';
+
+import '../analyzer.dart';
+
+const _desc = r'Use truncating division.';
+
+const _details = r'''
+**DO** use truncating division, '~/', instead of regular division ('/') followed
+by 'toInt()'.
+
+Dart features a "truncating division" operator which is the same operation as
+division followed by truncation, but which is more concise and expressive, and
+may be more performant on some platforms, for certain inputs.
+
+**BAD:**
+```dart
+var x = (2 / 3).toInt();
+```
+
+**GOOD:**
+```dart
+var x = 2 ~/ 3;
+```
+
+''';
+
+class UseTruncatingDivision extends LintRule {
+ static const LintCode code = LintCode(
+ 'use_truncating_division',
+ 'Use truncating division.',
+ correctionMessage:
+ "Try using truncating division, '~/', instead of regular division "
+ "('/') followed by 'toInt()'.",
+ );
+
+ UseTruncatingDivision()
+ : super(
+ name: 'use_truncating_division',
+ description: _desc,
+ details: _details,
+ categories: {LintRuleCategory.languageFeatureUsage});
+
+ @override
+ LintCode get lintCode => code;
+
+ @override
+ void registerNodeProcessors(
+ NodeLintRegistry registry, LinterContext context) {
+ var visitor = _Visitor(this);
+ registry.addBinaryExpression(this, visitor);
+ }
+}
+
+class _Visitor extends SimpleAstVisitor<void> {
+ final LintRule rule;
+
+ _Visitor(this.rule);
+
+ @override
+ void visitBinaryExpression(BinaryExpression node) {
+ if (node.operator.type != TokenType.SLASH) return;
+
+ // Return if the two operands are not each `int`.
+ var leftType = node.leftOperand.staticType;
+ if (leftType == null || !leftType.isDartCoreInt) return;
+
+ var rightType = node.rightOperand.staticType;
+ if (rightType == null || !rightType.isDartCoreInt) return;
+
+ // Return if the '/' operator is not defined in core, or if we don't know
+ // its static type.
+ var methodElement = node.staticElement;
+ if (methodElement == null) return;
+
+ var libraryElement = methodElement.library;
+ if (!libraryElement.isDartCore) return;
+
+ var parent = node.parent;
+ if (parent is! ParenthesizedExpression) return;
+
+ var outermostParentheses = parent.thisOrAncestorMatching(
+ (e) => e.parent is! ParenthesizedExpression)!
+ as ParenthesizedExpression;
+ var grandParent = outermostParentheses.parent;
+ if (grandParent is MethodInvocation &&
+ grandParent.methodName.name == 'toInt' &&
+ grandParent.argumentList.arguments.isEmpty) {
+ rule.reportLint(grandParent);
+ }
+ }
+}
diff --git a/pkg/linter/test/rules/use_truncating_division_test.dart b/pkg/linter/test/rules/use_truncating_division_test.dart
new file mode 100644
index 0000000..5d62584
--- /dev/null
+++ b/pkg/linter/test/rules/use_truncating_division_test.dart
@@ -0,0 +1,75 @@
+// Copyright (c) 2024, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:test_reflective_loader/test_reflective_loader.dart';
+
+import '../rule_test_support.dart';
+
+main() {
+ defineReflectiveSuite(() {
+ defineReflectiveTests(UseTruncatingDivisionTest);
+ });
+}
+
+@reflectiveTest
+class UseTruncatingDivisionTest extends LintRuleTest {
+ @override
+ String get lintRule => 'use_truncating_division';
+
+ test_double_divide_truncate() async {
+ await assertNoDiagnostics(r'''
+void f(double x, double y) {
+ (x / y).toInt();
+}
+''');
+ }
+
+ test_int_divide_truncate() async {
+ await assertDiagnostics(r'''
+void f(int x, int y) {
+ (x / y).toInt();
+}
+''', [
+ // TODO(srawlins): ASAP, remove this Hint.
+ error(HintCode.DIVISION_OPTIMIZATION, 25, 15),
+ lint(25, 15),
+ ]);
+ }
+
+ test_int_divide_truncate_moreParensAroundDivision() async {
+ await assertDiagnostics(r'''
+void f(int x, int y) {
+ (((x / y))).toInt();
+}
+''', [
+ // TODO(srawlins): ASAP, remove this Hint.
+ error(HintCode.DIVISION_OPTIMIZATION, 25, 19),
+ lint(25, 19),
+ ]);
+ }
+
+ test_int_divide_truncate_moreParensAroundOperands() async {
+ await assertDiagnostics(r'''
+void f(int x, int y) {
+ ((x + 1) / (y - 1)).toInt();
+}
+''', [
+ // TODO(srawlins): ASAP, remove this Hint.
+ error(HintCode.DIVISION_OPTIMIZATION, 25, 27),
+ lint(25, 27),
+ ]);
+ }
+
+ test_intExtensionType_divide_truncate() async {
+ await assertNoDiagnostics(r'''
+void f(ET x, int y) {
+ (x / y).toInt();
+}
+
+extension type ET(int it) {
+ int operator /(int other) => 7;
+}
+''');
+ }
+}