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;
+}
+''');
+  }
+}