Skip to content

Introduce RandomGeneratorNext{Double,Int,Long}WithOrigin Refaster rules#2087

Merged
Stephan202 merged 7 commits intomasterfrom
szumal/random-generator-rule
Mar 7, 2026
Merged

Introduce RandomGeneratorNext{Double,Int,Long}WithOrigin Refaster rules#2087
Stephan202 merged 7 commits intomasterfrom
szumal/random-generator-rule

Conversation

@szumal
Copy link
Contributor

@szumal szumal commented Jan 27, 2026

@github-actions
Copy link

github-actions bot commented Jan 27, 2026

Suggested commit message:

Introduce `RandomGeneratorNext{Double,Int,Long}WithOrigin` Refaster rules (#2087)

Add Refaster rules that rewrite `a + random.nextXxx(b)` to
`random.nextXxx(a, a + b)` for `double`, `int`, and `long`.

This favors the dedicated origin/bound overloads and helps avoid
silently producing values outside the intended domain.

@github-actions
Copy link

Looks good. No mutations were possible for these changes.
Mutation testing report by Pitest. Review any surviving mutants by inspecting the line comments under Files changed.

1 similar comment
@github-actions
Copy link

Looks good. No mutations were possible for these changes.
Mutation testing report by Pitest. Review any surviving mutants by inspecting the line comments under Files changed.

static final class RandomGeneratorNextIntWithOrigin {
@BeforeTemplate
int before(RandomGenerator random, int a, int b) {
return Refaster.anyOf(a + random.nextInt(b));

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[javac] reported by reviewdog 🐶
[RefasterAnyOfUsage] Refaster#anyOf should be passed at least two parameters

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AKA: No need to use this API if we're only writing from one before template variant.

Copy link
Collaborator

@mohamedsamehsalah mohamedsamehsalah left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make sure we run mvn fmt:format and run the full build ./run-full-build.sh

static final class RandomGeneratorNextIntWithOrigin {
@BeforeTemplate
int before(RandomGenerator random, int a, int b) {
return Refaster.anyOf(a + random.nextInt(b));
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AKA: No need to use this API if we're only writing from one before template variant.

@github-actions
Copy link

Looks good. No mutations were possible for these changes.
Mutation testing report by Pitest. Review any surviving mutants by inspecting the line comments under Files changed.

Copy link
Collaborator

@mohamedsamehsalah mohamedsamehsalah left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggesting:

Introduce `RandomGeneratorNextIntWithOrigin` refaster rule

}

ImmutableSet<Integer> testRandomGeneratorNextIntWithOrigin() {
return ImmutableSet.of(20 + new Random().nextInt(80));
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The return type can be an int.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated this and the test output 🫡

@github-actions
Copy link

Looks good. No mutations were possible for these changes.
Mutation testing report by Pitest. Review any surviving mutants by inspecting the line comments under Files changed.

Copy link
Member

@rickie rickie left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense but I think we can do even more :)


@AfterTemplate
int after(RandomGenerator random, int a, int b) {
return random.nextInt(a, a + b);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't we also apply this to the nextLong and nextDouble?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed, I think we can!

@github-actions
Copy link

github-actions bot commented Feb 1, 2026

Looks good. No mutations were possible for these changes.
Mutation testing report by Pitest. Review any surviving mutants by inspecting the line comments under Files changed.

@coderabbitai
Copy link

coderabbitai bot commented Feb 1, 2026

📝 Walkthrough

Walkthrough

Introduces three new refaster rules to RandomGeneratorRules that replace arithmetic expressions with origin-and-bound variant calls for RandomGenerator.nextInt(), nextDouble(), and nextLong() methods. Includes corresponding test cases for both input and expected output scenarios.

Changes

Cohort / File(s) Summary
Refaster Rules Implementation
error-prone-contrib/src/main/java/tech/picnic/errorprone/refasterrules/RandomGeneratorRules.java
Added three new refaster rule classes: RandomGeneratorNextIntWithOrigin, RandomGeneratorNextDoubleWithOrigin, and RandomGeneratorNextLongWithOrigin. Each rule rewrites patterns of form a + random.nextX(b) to random.nextX(a, a + b) where X is Int, Double, or Long.
Test Input Cases
error-prone-contrib/src/test/resources/tech/picnic/errorprone/refasterrules/RandomGeneratorRulesTestInput.java
Added three test input methods demonstrating the before-transformation patterns: testRandomGeneratorNextIntWithOrigin(), testRandomGeneratorNextDoubleWithOrigin(), and testRandomGeneratorNextLongWithOrigin(), each using arithmetic addition with a random generator call.
Test Output Cases
error-prone-contrib/src/test/resources/tech/picnic/errorprone/refasterrules/RandomGeneratorRulesTestOutput.java
Added three expected output test methods showing the after-transformation patterns using origin-and-bound method variants: nextInt(20, 100), nextDouble(2.0, 10.0), and nextLong(2L, 10L).

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ❓ Inconclusive The description only contains a link to an issue without explaining what changes are being made or why, providing minimal context about the pull request. Add a brief description of the changes being made, such as which rules are being added and the refactoring pattern they implement.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main change: introducing three new Refaster rules for RandomGenerator methods with origin parameters.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch szumal/random-generator-rule

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In
`@error-prone-contrib/src/main/java/tech/picnic/errorprone/refasterrules/RandomGeneratorRules.java`:
- Around line 82-123: The after-templates in RandomGeneratorNextIntWithOrigin,
RandomGeneratorNextDoubleWithOrigin, and RandomGeneratorNextLongWithOrigin
duplicate the expression `a` (risking double evaluation) and can cause overflow
for `a + b` (changing wrapping semantics to exceptions); add Refaster guards so
these rules only apply when `a` is a stable/pure expression (no side effects or
method calls) and when `a + b` cannot overflow: for
RandomGeneratorNextIntWithOrigin require b >= 0 and a <= Integer.MAX_VALUE - b;
for RandomGeneratorNextLongWithOrigin require b >= 0 and a <= Long.MAX_VALUE -
b; for RandomGeneratorNextDoubleWithOrigin require Double.isFinite(a) &&
Double.isFinite(b) && b >= 0 && Double.isFinite(a + b); implement these as guard
methods (e.g., isStable(a), nonNegative(b), noOverflowInt(a,b),
noOverflowLong(a,b), finiteDoubleBounds(a,b)) referenced from the rule classes
so the refactoring is only applied when safe.

Comment on lines +82 to +123
static final class RandomGeneratorNextIntWithOrigin {
@BeforeTemplate
int before(RandomGenerator random, int a, int b) {
return a + random.nextInt(b);
}

@AfterTemplate
int after(RandomGenerator random, int a, int b) {
return random.nextInt(a, a + b);
}
}

/**
* Prefer {@link RandomGenerator#nextDouble(double origin, double bound)} over more contrived
* alternatives.
*/
static final class RandomGeneratorNextDoubleWithOrigin {
@BeforeTemplate
double before(RandomGenerator random, double a, double b) {
return a + random.nextDouble(b);
}

@AfterTemplate
double after(RandomGenerator random, double a, double b) {
return random.nextDouble(a, a + b);
}
}

/**
* Prefer {@link RandomGenerator#nextLong(long origin, long bound)} over more contrived
* alternatives.
*/
static final class RandomGeneratorNextLongWithOrigin {
@BeforeTemplate
long before(RandomGenerator random, long a, long b) {
return a + random.nextLong(b);
}

@AfterTemplate
long after(RandomGenerator random, long a, long b) {
return random.nextLong(a, a + b);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's examine the file in question to see the complete rules with any guards/constraints
fd -type f -name "RandomGeneratorRules.java" -exec wc -l {} \;

Repository: PicnicSupermarket/error-prone-support

Length of output: 251


🏁 Script executed:

# Search for the RandomGenerator interface to understand its contract
fd -type f -name "*.java" | xargs grep -l "interface RandomGenerator" | head -5

Repository: PicnicSupermarket/error-prone-support

Length of output: 251


🏁 Script executed:

# Look for nextInt(int origin, int bound) signature documentation
rg "nextInt.*origin.*bound" -A 5 -B 5

Repository: PicnicSupermarket/error-prone-support

Length of output: 1475


🏁 Script executed:

# Check if there are any guards or constraints in the Refaster rules
cat error-prone-contrib/src/main/java/tech/picnic/errorprone/refasterrules/RandomGeneratorRules.java

Repository: PicnicSupermarket/error-prone-support

Length of output: 4466


🏁 Script executed:

# Search for RandomGenerator usage in tests to understand the expected behavior
fd -name "*RandomGenerator*Test*" -o -name "*Test*RandomGenerator*"

Repository: PicnicSupermarket/error-prone-support

Length of output: 311


🏁 Script executed:

# Look for any documentation or tests that show the contract for nextInt(origin, bound)
rg "nextInt.*origin.*bound" -B 3 -A 10

Repository: PicnicSupermarket/error-prone-support

Length of output: 1842


🏁 Script executed:

# Search for RandomGenerator interface in the repository
rg "interface RandomGenerator"

Repository: PicnicSupermarket/error-prone-support

Length of output: 63


🌐 Web query:

java.util.random.RandomGenerator nextInt origin bound contract documentation

💡 Result:

java.util.random.RandomGenerator#nextInt(int origin, int bound) returns a pseudorandomly chosen int in the range [origin, bound) — i.e., origin is inclusive, bound is exclusive. It is specified to produce a uniform distribution over that range (assuming the generator’s nextInt() is uniform). Throws IllegalArgumentException if origin >= bound. [1]

Related contract points (same interface):

  • nextInt(int bound) returns a value in [0, bound) and throws IllegalArgumentException if bound <= 0. [2]
  • The interface’s default implementations may call nextInt() multiple times (“rejection sampling”) to achieve uniformity. [1]

Sources: Oracle JDK API docs for RandomGenerator (Java SE 25) [1] and (Java SE 21) [2].


🏁 Script executed:

# Look for test files to understand how these rules are used and tested
fd "RandomGenerator" --extension java | grep -i test

Repository: PicnicSupermarket/error-prone-support

Length of output: 303


🏁 Script executed:

# Check for any Refaster documentation or guards in the codebase
rg "Precondition|Guard" error-prone-contrib/src/main/java/tech/picnic/errorprone/refasterrules/ -B 2 -A 2 | head -40

Repository: PicnicSupermarket/error-prone-support

Length of output: 5073


🏁 Script executed:

# Look for how parameters are used in other Refaster rules to understand evaluation semantics
rg "@BeforeTemplate" -A 5 error-prone-contrib/src/main/java/tech/picnic/errorprone/refasterrules/ | head -60

Repository: PicnicSupermarket/error-prone-support

Length of output: 6559


🌐 Web query:

Refaster parameter evaluation multiple occurrences semantics double evaluation

💡 Result:

In Refaster, a @BeforeTemplate method parameter is an expression variable: it binds to (i.e., captures) the matched AST expression from user code, and Refaster generates the replacement by substituting that captured expression into the @AfterTemplate. [1]

As a result, if you reference the same parameter multiple times in @AfterTemplate, Refaster will duplicate the captured expression multiple times in the generated code—so any side effects or expensive work in that expression can be evaluated multiple times after the rewrite (macro-like behavior), unless you structure the @AfterTemplate to avoid that. [1][2]

Example (illustrative):

  • If x matches compute() and your @AfterTemplate uses x twice, the output will contain compute() twice.

Sources: Refaster substitution description and “expression variables” semantics [1], Refaster template parameter-as-“any expression” explanation [2].

[1] https://research.google.com/pubs/archive/41876.pdf
[2] https://errorprone.info/docs/refaster


🏁 Script executed:

# Check the test files to see what cases are tested
cat error-prone-contrib/src/test/resources/tech/picnic/errorprone/refasterrules/RandomGeneratorRulesTestInput.java

Repository: PicnicSupermarket/error-prone-support

Length of output: 1507


🏁 Script executed:

# Check the test output to see expected transformations
cat error-prone-contrib/src/test/resources/tech/picnic/errorprone/refasterrules/RandomGeneratorRulesTestOutput.java

Repository: PicnicSupermarket/error-prone-support

Length of output: 1408


🏁 Script executed:

# Look for any discussion about these rules or overflow concerns
rg "nextInt.*origin.*bound|nextLong.*origin.*bound|nextDouble.*origin.*bound" -B 5 -A 5 | head -100

Repository: PicnicSupermarket/error-prone-support

Length of output: 4244


🌐 Web query:

RandomGenerator nextInt(int origin, int bound) overflow IllegalArgumentException int addition wraparound

💡 Result:

In Java’s RandomGenerator.nextInt(int origin, int bound), an IllegalArgumentException is thrown only when origin >= bound (a plain signed int comparison), not because of arithmetic overflow in computing the range. The spec for RandomGenerator.nextInt(origin, bound) states it checks that origin is less than bound, then generates a uniformly distributed value in [origin, bound). [1]

The usual pitfall is computing the range as int n = bound - origin and then doing nextInt(n) + origin: if bound - origin overflows (wraps negative/zero), n is no longer a valid positive bound. The JDK’s reference algorithm accounts for this: it computes int n = bound - origin; and only uses nextInt(n) + origin when n > 0; otherwise (the “range not representable as int” case caused by overflow), it falls back to rejection sampling (nextInt() until origin <= r < bound), avoiding the wraparound-add bug while still honoring the requested interval. [2]

Sources:
[1] Java SE RandomGenerator API docs (nextInt(int origin, int bound) contract and exception) (docs.oracle.com)
[2] Java SE Random docs showing the reference algorithm with the n > 0 / “range not representable as int” fallback (docs.oracle.com)

Citations:


Both parameter double-evaluation and overflow behavior concerns are valid; add guards or constraints.

In the after templates, a is referenced twice (a and a + b). If a matches an expression with side effects, Refaster will duplicate it, causing the expression to evaluate multiple times—changing program semantics.

Additionally, a + b can overflow for int/long types. In the before template, this naturally wraps; in the after template, if a + b overflows and violates the origin < bound precondition, nextInt(origin, bound) throws IllegalArgumentException instead of wrapping, changing behavior.

Prevent these issues by guarding a and b to be stable, non-side-effect expressions (or introduce placeholder/guard restrictions) and ensure a + b cannot overflow (e.g., constrain ranges or use explicit bounds checks).

🤖 Prompt for AI Agents
In
`@error-prone-contrib/src/main/java/tech/picnic/errorprone/refasterrules/RandomGeneratorRules.java`
around lines 82 - 123, The after-templates in RandomGeneratorNextIntWithOrigin,
RandomGeneratorNextDoubleWithOrigin, and RandomGeneratorNextLongWithOrigin
duplicate the expression `a` (risking double evaluation) and can cause overflow
for `a + b` (changing wrapping semantics to exceptions); add Refaster guards so
these rules only apply when `a` is a stable/pure expression (no side effects or
method calls) and when `a + b` cannot overflow: for
RandomGeneratorNextIntWithOrigin require b >= 0 and a <= Integer.MAX_VALUE - b;
for RandomGeneratorNextLongWithOrigin require b >= 0 and a <= Long.MAX_VALUE -
b; for RandomGeneratorNextDoubleWithOrigin require Double.isFinite(a) &&
Double.isFinite(b) && b >= 0 && Double.isFinite(a + b); implement these as guard
methods (e.g., isStable(a), nonNegative(b), noOverflowInt(a,b),
noOverflowLong(a,b), finiteDoubleBounds(a,b)) referenced from the rule classes
so the refactoring is only applied when safe.

@github-actions
Copy link

github-actions bot commented Feb 4, 2026

Looks good. No mutations were possible for these changes.
Mutation testing report by Pitest. Review any surviving mutants by inspecting the line comments under Files changed.

Copy link
Member

@rickie rickie left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Congrats on your PR @szumal ! Looks good to me :)

Added a small commit with a reordering of the rules. One thing we could still consider is moving the int related ones as first ones. I think that's slightly more inline with how we usually have that. CC: @Stephan202

}

int testRandomGeneratorNextIntWithOrigin() {
return 20 + new Random().nextInt(80);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We usually use even simpler test values :)

@rickie rickie added this to the 0.29.0 milestone Feb 4, 2026
@github-actions
Copy link

github-actions bot commented Feb 4, 2026

Looks good. No mutations were possible for these changes.
Mutation testing report by Pitest. Review any surviving mutants by inspecting the line comments under Files changed.

@github-actions
Copy link

Looks good. No mutations were possible for these changes.
Mutation testing report by Pitest. Review any surviving mutants by inspecting the line comments under Files changed.

Copy link
Member

@Stephan202 Stephan202 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rebased and added a commit.

Suggested commit message:

Introduce `RandomGeneratorNext{Double,Int,Long}WithOrigin` Refaster rules (#2087)

Resolves #2047.

I'll let @rickie have a final look at the changes and then merge.

One thing we could still consider is moving the int related ones as first ones. I think that's slightly more inline with how we usually have that. CC: @Stephan202

Perhaps we should, but the current order matches what is already there, which is lexicographical. Let's keep it as-is for now; we may even settle on this in more place.

Comment on lines +34 to +37
/**
* Prefer {@link RandomGenerator#nextDouble(double origin, double bound)} over more contrived
* alternatives.
*/
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would not say that the alternative is more contrived. But in case of large origin or bound, the replacement code is more likely to yield a correct value distribution, or throw an IllegalArgumentException in case of illogical bounds.


@AfterTemplate
double after(RandomGenerator random, double a, double b) {
return random.nextDouble(a, a + b);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's document CodeRabbit's feedback about a having side-effects or being expensive to compute.

Comment on lines +65 to +67
/**
* Prefer {@link RandomGenerator#nextInt(int origin, int bound)} over more contrived alternatives.
*/
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here too there are more substantial benefits: the replacement code won't silently cause overflow.

import com.sun.source.tree.UnaryTree;

/** A matcher of expressions that may a non-trivial amount of computation. */
/** A matcher of expressions that may require a non-trivial amount of computation. */
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I noticed this while briefly considering adding @NotMatches(RequiresComputation.class).

@Stephan202 Stephan202 changed the title Introducing a new RandomGeneratorRule Introduce RandomGeneratorNext{Double,Int,Long}WithOrigin Refaster rules Mar 1, 2026
@github-actions
Copy link

github-actions bot commented Mar 1, 2026

Looks good. No mutations were possible for these changes.
Mutation testing report by Pitest. Review any surviving mutants by inspecting the line comments under Files changed.

Copy link
Member

@rickie rickie left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Second time approval, nice changes @Stephan202 !

@github-actions
Copy link

github-actions bot commented Mar 7, 2026

Looks good. No mutations were possible for these changes.
Mutation testing report by Pitest. Review any surviving mutants by inspecting the line comments under Files changed.

@sonarqubecloud
Copy link

sonarqubecloud bot commented Mar 7, 2026

@Stephan202 Stephan202 merged commit ae09721 into master Mar 7, 2026
19 checks passed
@Stephan202 Stephan202 deleted the szumal/random-generator-rule branch March 7, 2026 17:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Development

Successfully merging this pull request may close these issues.

Prefer RandomGenerator#nextInt(source, bound) over less accurate alternatives

4 participants