Skip to content

Introducing a new RandomGeneratorRule#2087

Open
szumal wants to merge 6 commits intomasterfrom
szumal/random-generator-rule
Open

Introducing a new RandomGeneratorRule#2087
szumal wants to merge 6 commits intomasterfrom
szumal/random-generator-rule

Conversation

@szumal
Copy link

@szumal szumal commented Jan 27, 2026

@szumal szumal force-pushed the szumal/random-generator-rule branch from ac2cbe7 to bd4c84d Compare January 27, 2026 12:53
@github-actions
Copy link

github-actions bot commented Jan 27, 2026

Suggested commit message:

Introduce RandomGenerator origin/bound Refaster rules (#2087)

Add Refaster rules that prefer origin/bound overloads for
`RandomGenerator#nextDouble`, `nextInt`, and `nextLong` when a
value is offset by a random bound. This simplifies common
patterns and aligns with newer JDK APIs.

@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
Contributor

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
Contributor

@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
Contributor

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
Contributor

@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
Contributor

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
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
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 | ❌ 3
❌ Failed checks (1 warning, 2 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.
Title check ❓ Inconclusive The title refers to 'a new RandomGeneratorRule' but the changeset actually introduces three new rule classes (RandomGeneratorNextIntWithOrigin, RandomGeneratorNextDoubleWithOrigin, RandomGeneratorNextLongWithOrigin) for handling origin/bound variants. Consider making the title more specific: e.g., 'Add RandomGenerator origin/bound rules' or 'Add RandomGenerator nextInt/nextDouble/nextLong with origin rules' to better reflect the actual scope of changes.
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.

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

✨ Finishing touches
  • 📝 Generate docstrings
🧪 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
@sonarqubecloud
Copy link

sonarqubecloud bot commented 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.

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 using RandomGenerator#nextInt(source, bound) over more contrived alternatives

3 participants