|
| 1 | +package errorprone; |
| 2 | + |
| 3 | +import com.google.auto.service.AutoService; |
| 4 | +import com.google.errorprone.BugPattern; |
| 5 | +import com.google.errorprone.VisitorState; |
| 6 | +import com.google.errorprone.bugpatterns.BugChecker; |
| 7 | +import com.google.errorprone.matchers.Description; |
| 8 | +import com.google.errorprone.util.ASTHelpers; |
| 9 | +import com.sun.source.tree.IdentifierTree; |
| 10 | +import com.sun.source.tree.MemberReferenceTree; |
| 11 | +import com.sun.source.tree.MemberSelectTree; |
| 12 | +import com.sun.source.tree.MethodInvocationTree; |
| 13 | +import com.sun.source.tree.Tree; |
| 14 | +import com.sun.tools.javac.code.Symbol; |
| 15 | + |
| 16 | +/** |
| 17 | + * Forbids any direct use of {@code java.lang.Math}. |
| 18 | + * |
| 19 | + * <p>{@code java.lang.Math} permits the JIT to use platform-specific intrinsics for some |
| 20 | + * methods (notably the transcendental and floating-point ones), so results are not |
| 21 | + * guaranteed to be bit-for-bit identical across CPUs / JVMs. In a consensus system that |
| 22 | + * non-determinism can fork the chain. All math must therefore go through |
| 23 | + * {@code org.tron.common.math.StrictMathWrapper}, which is backed by {@code java.lang.StrictMath} |
| 24 | + * and produces reproducible results. |
| 25 | + * |
| 26 | + * <p>This checker replaces the previous regex-based {@code .github/workflows/math-check.yml} |
| 27 | + * scan. It resolves symbols on the type-attributed AST, so it has no false positives from |
| 28 | + * strings/comments or unrelated classes named {@code Math}, and it catches every usage form: |
| 29 | + * <ul> |
| 30 | + * <li>direct calls: {@code Math.max(a, b)}</li> |
| 31 | + * <li>fully-qualified calls: {@code java.lang.Math.max(a, b)}</li> |
| 32 | + * <li>statically-imported calls: {@code import static java.lang.Math.max; ... max(a, b)}</li> |
| 33 | + * <li>method references: {@code Math::max}</li> |
| 34 | + * <li>field access: {@code Math.PI}, {@code Math.E}</li> |
| 35 | + * <li>statically-imported fields used bare: {@code import static java.lang.Math.PI; ... PI}</li> |
| 36 | + * <li>class literals (reflection back door): {@code Math.class}</li> |
| 37 | + * </ul> |
| 38 | + * |
| 39 | + * <p>{@code java.lang.StrictMath} itself is intentionally allowed — it is the deterministic |
| 40 | + * primitive that {@code StrictMathWrapper} and the deprecated {@code MathWrapper} are built on. |
| 41 | + * Those wrappers legitimately call {@code java.lang.Math}/{@code StrictMath} and are exempted via |
| 42 | + * {@code @SuppressWarnings("ForbidJavaLangMath")}. |
| 43 | + */ |
| 44 | +@AutoService(BugChecker.class) |
| 45 | +@BugPattern( |
| 46 | + name = "ForbidJavaLangMath", |
| 47 | + summary = "Direct use of java.lang.Math is forbidden: its results are not guaranteed to be " |
| 48 | + + "bit-for-bit identical across platforms, which can break consensus. Use " |
| 49 | + + "org.tron.common.math.StrictMathWrapper instead.", |
| 50 | + severity = BugPattern.SeverityLevel.ERROR |
| 51 | +) |
| 52 | +public class ForbidJavaLangMath extends BugChecker |
| 53 | + implements BugChecker.MethodInvocationTreeMatcher, |
| 54 | + BugChecker.MemberReferenceTreeMatcher, |
| 55 | + BugChecker.MemberSelectTreeMatcher, |
| 56 | + BugChecker.IdentifierTreeMatcher { |
| 57 | + |
| 58 | + private static final String JAVA_LANG_MATH = "java.lang.Math"; |
| 59 | + |
| 60 | + @Override |
| 61 | + public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) { |
| 62 | + return flagIfMathMember(ASTHelpers.getSymbol(tree), tree); |
| 63 | + } |
| 64 | + |
| 65 | + @Override |
| 66 | + public Description matchMemberReference(MemberReferenceTree tree, VisitorState state) { |
| 67 | + return flagIfMathMember(ASTHelpers.getSymbol(tree), tree); |
| 68 | + } |
| 69 | + |
| 70 | + @Override |
| 71 | + public Description matchMemberSelect(MemberSelectTree tree, VisitorState state) { |
| 72 | + // Type-level references such as the class literal `Math.class` resolve to the Math |
| 73 | + // ClassSymbol rather than a member, and are a back door to java.lang.Math via reflection |
| 74 | + // (e.g. Math.class.getMethod("sin").invoke(...)). Flag them explicitly. |
| 75 | + if (tree.getIdentifier().contentEquals("class") |
| 76 | + && isJavaLangMathClass(ASTHelpers.getSymbol(tree.getExpression()))) { |
| 77 | + return describeMatch(tree); |
| 78 | + } |
| 79 | + // Method selects (Math.max) are already reported via matchMethodInvocation / |
| 80 | + // matchMemberReference; only flag field selects (Math.PI, Math.E) here to avoid |
| 81 | + // double-reporting the same usage. |
| 82 | + Symbol sym = ASTHelpers.getSymbol(tree); |
| 83 | + if (!(sym instanceof Symbol.VarSymbol)) { |
| 84 | + return Description.NO_MATCH; |
| 85 | + } |
| 86 | + return flagIfMathMember(sym, tree); |
| 87 | + } |
| 88 | + |
| 89 | + @Override |
| 90 | + public Description matchIdentifier(IdentifierTree tree, VisitorState state) { |
| 91 | + // Catches statically-imported constants referenced bare (e.g. `import static |
| 92 | + // java.lang.Math.PI; ... double c = PI;`). Statically-imported *methods* are caught by |
| 93 | + // matchMethodInvocation, so restrict this to fields to avoid double-reporting. |
| 94 | + Symbol sym = ASTHelpers.getSymbol(tree); |
| 95 | + if (!(sym instanceof Symbol.VarSymbol)) { |
| 96 | + return Description.NO_MATCH; |
| 97 | + } |
| 98 | + return flagIfMathMember(sym, tree); |
| 99 | + } |
| 100 | + |
| 101 | + private static boolean isJavaLangMathClass(Symbol sym) { |
| 102 | + return sym instanceof Symbol.ClassSymbol |
| 103 | + && ((Symbol.ClassSymbol) sym).getQualifiedName().contentEquals(JAVA_LANG_MATH); |
| 104 | + } |
| 105 | + |
| 106 | + private Description flagIfMathMember(Symbol sym, Tree tree) { |
| 107 | + if (sym == null) { |
| 108 | + return Description.NO_MATCH; |
| 109 | + } |
| 110 | + Symbol.ClassSymbol enclosingClass = ASTHelpers.enclosingClass(sym); |
| 111 | + if (enclosingClass == null) { |
| 112 | + return Description.NO_MATCH; |
| 113 | + } |
| 114 | + if (enclosingClass.getQualifiedName().contentEquals(JAVA_LANG_MATH)) { |
| 115 | + return describeMatch(tree); |
| 116 | + } |
| 117 | + return Description.NO_MATCH; |
| 118 | + } |
| 119 | +} |
0 commit comments