[javac] Add compiler check for integer division in floating-point contexts (#8885)

Useful for catching bad behavior when doing math or configuring ratios
with integers. Errors can be turned off with
`@SuppressWarnings("IntegerDivision")`

```java
double kV = 12 / 6000; // error: integer division in a floating-point context
double kV = 12. / 6000; // OK
double kV = 12 / 6000.; // OK

int kV = 12 / 6000; // OK, but evaluates to 0

double ratio = 42 / 12; // error: integer division in a floating-point context

@SuppressWarnings("IntegerDivision")
double ratio = 42 / 12; // OK, evaluates to 2
```

```
Execution failed for task ':developerRobot:compileJava'.
> Compilation failed; see the compiler output below.
  /Users/sam/code/wpilib/allwpilib/developerRobot/src/main/java/wpilib/robot/Robot.java:10: error: integer division in a floating-point context
    double kV = 12 / 6000;
                   ^
  1 error
```

Also adds the compiler plugin as a dependency to the developerRobot
project for dev testing
This commit is contained in:
Sam Carlberg
2026-06-05 00:56:22 -04:00
committed by GitHub
parent 8353b95507
commit 98c03baf54
4 changed files with 408 additions and 0 deletions

View File

@@ -0,0 +1,163 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
package org.wpilib.javacplugin;
import com.sun.source.tree.AssignmentTree;
import com.sun.source.tree.BinaryTree;
import com.sun.source.tree.CompilationUnitTree;
import com.sun.source.tree.ExpressionTree;
import com.sun.source.tree.MethodTree;
import com.sun.source.tree.ParenthesizedTree;
import com.sun.source.tree.ReturnTree;
import com.sun.source.tree.Tree;
import com.sun.source.tree.Tree.Kind;
import com.sun.source.tree.TypeCastTree;
import com.sun.source.tree.VariableTree;
import com.sun.source.util.JavacTask;
import com.sun.source.util.TaskEvent;
import com.sun.source.util.TaskListener;
import com.sun.source.util.TreePath;
import com.sun.source.util.TreeScanner;
import com.sun.source.util.Trees;
import java.util.HashSet;
import java.util.Set;
import javax.lang.model.type.TypeMirror;
import javax.tools.Diagnostic;
/**
* Detects integer division operations in Java source code and marks them as errors.
*
* <pre>{@code
* double x = 1 / 2; // -> error: integer division for a floating-point number
* double x = (double) 1 / 2; // -> ok (casting an operand to a double)
* double x = (double) (1 / 2); // -> error: integer division for a floating-point number
* int x = 1 / 2; // -> ok (in an integer context)
* }</pre>
*/
public class IntegerDivisionDetector implements TaskListener {
private final JavacTask m_task;
private final Set<CompilationUnitTree> m_visitedCUs = new HashSet<>();
public IntegerDivisionDetector(JavacTask task) {
m_task = task;
}
@Override
public void finished(TaskEvent e) {
// We override `finished` instead of `started` because we want to run after the
// ANALYZE attribution phase has completed and assigned types to elements in the AST
// Track the visited CUs to avoid re-processing the same CU multiple times when we call
// `Trees.getElement()` on a tree path.
if (e.getKind() == TaskEvent.Kind.ANALYZE && m_visitedCUs.add(e.getCompilationUnit())) {
e.getCompilationUnit().accept(new Scanner(e.getCompilationUnit()), null);
}
}
private final class Scanner extends TreeScanner<Void, Void> {
private final CompilationUnitTree m_root;
private final Trees m_trees;
Scanner(CompilationUnitTree compilationUnit) {
m_root = compilationUnit;
m_trees = Trees.instance(m_task);
}
@Override
public Void visitBinary(BinaryTree node, Void unused) {
if (node.getKind() != Kind.DIVIDE) {
return super.visitBinary(node, unused);
}
TypeMirror type = m_trees.getTypeMirror(m_trees.getPath(m_root, node));
if (!isIntegerType(type)) {
return super.visitBinary(node, unused);
}
// It's integer division. Now check the context.
if (isFloatContext(m_trees.getPath(m_root, node))) {
if (Suppressions.hasSuppression(
m_trees, m_trees.getPath(m_root, node), "IntegerDivision")) {
return super.visitBinary(node, unused);
}
m_trees.printMessage(
Diagnostic.Kind.ERROR, "integer division in a floating-point context", node, m_root);
}
return super.visitBinary(node, unused);
}
private boolean isIntegerType(TypeMirror type) {
if (type == null) {
return false;
}
return switch (type.getKind()) {
case BYTE, CHAR, SHORT, INT, LONG -> true;
default -> false;
};
}
private boolean isFloatType(TypeMirror type) {
if (type == null) {
return false;
}
return switch (type.getKind()) {
case FLOAT, DOUBLE -> true;
default -> false;
};
}
private boolean isFloatContext(TreePath path) {
TreePath parentPath = path.getParentPath();
while (parentPath != null && parentPath.getLeaf() instanceof ParenthesizedTree) {
parentPath = parentPath.getParentPath();
}
if (parentPath == null) {
return false;
}
Tree parent = parentPath.getLeaf();
switch (parent) {
case AssignmentTree assignment -> {
if (getExpression(assignment.getExpression()) == path.getLeaf()) {
return isFloatType(
m_trees.getTypeMirror(m_trees.getPath(m_root, assignment.getVariable())));
}
}
case VariableTree variable -> {
if (variable.getInitializer() != null
&& getExpression(variable.getInitializer()) == path.getLeaf()) {
return isFloatType(m_trees.getTypeMirror(m_trees.getPath(m_root, variable.getType())));
}
}
case ReturnTree _ -> {
TreePath p = parentPath;
while (p != null && !(p.getLeaf() instanceof MethodTree)) {
p = p.getParentPath();
}
if (p != null && p.getLeaf() instanceof MethodTree method) {
return isFloatType(
m_trees.getTypeMirror(m_trees.getPath(m_root, method.getReturnType())));
}
}
case TypeCastTree cast -> {
return isFloatType(m_trees.getTypeMirror(m_trees.getPath(m_root, cast.getType())));
}
case null, default -> {
return false;
}
}
return false;
}
private Tree getExpression(ExpressionTree tree) {
while (tree instanceof ParenthesizedTree p) {
tree = p.getExpression();
}
return tree;
}
}
}

View File

@@ -26,6 +26,7 @@ public class WPILibJavacPlugin implements Plugin {
task.addTaskListener(new CoroutineYieldInLoopDetector(task));
task.addTaskListener(new CodeAfterCoroutineParkDetector(task));
task.addTaskListener(new IncorrectCoroutineUseDetector(task));
task.addTaskListener(new IntegerDivisionDetector(task));
}
@Override