The year 2025 is coming to an end. Minecraft mods, the Catalan language, and non-obvious interactions with the ternary operator—how many things our analyzer has encountered! So it's high time to share the highlights with you. Meet the top 10 Java errors that PVS-Studio analyzer detected in open-source projects in 2025.
Introduction
This year, we checked many open-source projects and wrote articles about them.
Now, we've selected ten of the most curious bugs and bizarre code fragments. The selection and the ranking are, of course, subjective and based on our own sense of coolness. And this article is just that collection of those findings.
Well, it's time~ Let's go!
10th place. Right to left, left to right
We open our top with a case from LanguageTool. In a way, the project resonates with us: just like PVS-Studio, it analyzes languages—only natural ones, not artificial.
The funny thing is that this error relates to our analyzer. However, we find it so amusing that we simply can't skip it. Really, what kind of New Year would it be without a bit of mischief?
Look at the code fragment:
public String getEnclitic(AnalyzedToken token) {
....
if (word.endsWith("ه")) {
suffix = "ه";
....
else if ((word.equals("عني") ||
word.equals("مني")) &&
word.endsWith("ني") // <=
) {
suffix = "ني";
}
....
}
The PVS-Studio warning: V6007 Expression 'word.endsWith("ني")' is always false. ArabicTagger.java 428
If we look at string constants the way native speakers of left-to-right (LTR) languages—like English or French—usually do, the analyzer's warning does seem fair. However, there's an important nuance: Arabic is a right-to-left (RTL) language, and this means that where non-Arabic speakers expect the end of a literal, they'll actually see its beginning. Well, as it turned out, our analyzer can't speak Arabic.
Incidentally, if we know that there is an RTL writing, we realize that the condition is actually always true, not false.
That got us curious: how does Java handle right-to-left languages at all? Everything is easy as pie: Java handles Unicode instead of the text visualization. In Unicode, characters from any language—whether written from left to right or right to left—are stored in logical order, i.e., in the order in which native speakers read and write them.
This means that Java doesn't need to perform any special conversions to handle RTL text. The string remains a regular sequence of Unicode code points, and operations such as obtaining its length, accessing characters, or extracting a substring work the same for all languages.
Therefore, for such a check:
System.out.println("مرحباً بالجميع".endsWith("بالجميع"));
The result will be true, although if we read from left to right, the string begins, rather than ends, with "بالجميع".
Still, since the analyzer is technically wrong here, we're placing it at 10th place in our ranking.
If you're curious to learn more, you can read the full story of how we analyzed LanguageTool in this article.
9th place. Catalan language
LanguageTool appears in our ranking once again!
The Catalan language module has the removeOldDiacritics() method, which neatly corrects outdated spelling forms and removes unnecessary diacritics. For example, it converts "adéu" to "adeu", "dóna" to "dona", "vénen" to "venen" and so on:
private String removeOldDiacritics(String s) {
return s
.replace("contrapèl", "contrapel")
.replace("Contrapèl", "Contrapel")
.replace("vés", "ves")
.replace("féu", "feu")
.replace("desféu", "desfeu")
.replace("adéu", "adeu")
.replace("dóna", "dona")
.replace("dónes", "dones")
.replace("sóc", "soc")
.replace("vénen", "venen")
.replace("véns", "véns") // <=
.replace("fóra", "fora")
.replace("Vés", "Ves")
.replace("Féu", "Feu")
.replace("Desféu", "Desfeu")
.replace("Adéu", "Adeu")
.replace("Dóna", "Dona")
.replace("Dónes", "Dones")
.replace("Sóc", "Soc")
.replace("Vénen", "Venen")
.replace("Véns", "Vens")
.replace("Fóra", "Fora");
}
At first glance, it seems okay: it works, corrects, and doesn't bother anyone.
However, the analyzer has another point: V6009 Function 'replace' receives an odd argument. The '" véns " argument was passed several times. Catalan.java 453
The problem that the method replaces "véns" with "véns", in other words, nothing is replaced at all. Most likely, when developers worked with this block, they simply copied the original word and replaced the letter, but in this case, forgot to change the letter in the second argument:
....
.replace("véns", "vens")
....
Since our analyzer for artificial languages managed to detect an error in their analyzer for natural languages, this warning earns its place in our top list.
8th place. Last line effect
Let's go further, now we talk about the error from Elasticsearch.
Look at the code fragment:
@Override
public boolean equals(Object obj) {
....
KeyedFilter other = (KeyedFilter) obj;
return Objects.equals(keys, other.keys)
&& Objects.equals(timestamp, other.timestamp)
&& Objects.equals(tiebreaker, other.tiebreaker)
&& Objects.equals(child(), other.child())
&& isMissingEventFilter == isMissingEventFilter; // <=
}
The PVS-Studio warning: V6001 There are identical sub-expressions 'isMissingEventFilter' to the left and to the right of the '==' operator. KeyedFilter.java 116
As in the previous example, we're dealing with a typo, but with a slight nuance: the error occurs in the last block line of identical code.
This situation occurs quite often during development, so often that developers have given it a name, the last line effect. Even within this project, this isn't a single case: a similar error appears in the last line of another block in the identical code:
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
IndexError that = (IndexError) o;
return indexName.equals(that.indexName)
&& Arrays.equals(shardIds, that.shardIds)
&& errorType == that.errorType
&& message.equals(that.message)
&& stallTimeSeconds == stallTimeSeconds; // <=
}
The PVS-Studio warning: V6001 There are identical sub-expressions 'stallTimeSeconds' to the left and to the right of the '==' operator. IndexError.java 147
If you want to learn more about such an effect, we invite you to read the following articles:
- our first encounter with it: The Last Line Effect;
- a more detailed discussion about the phenomenon: The last line effect explained.
As this error demonstrates such a curious phenomenon, we award it a place in our top list.
7th place. Lost in localization
Let's move on to the seventh position.
We've already looked at three Java errors, but we haven't encountered Minecraft yet. It's time to fix that!
Look at the code fragment from CustomNPC+:
private String getTimePast() {
....
if(selected.timePast > 3600000){
int hours = (int) (selected.timePast / 3600000);
if(hours == 1)
return hours + " " +
StatCollector.translateToLocal("mailbox.hour");
else
return hours + " " +
StatCollector.translateToLocal("mailbox.hours");
}
int minutes = (int) (selected.timePast / 60000);
if(minutes == 1)
return minutes + " " +
StatCollector.translateToLocal("mailbox.minutes");
else
return minutes + " " +
StatCollector.translateToLocal("mailbox.minutes");
}
The PVS-Studio warning: V6004 The 'then' statement is equivalent to the 'else' statement. GuiMailbox.java 76
Here we can see a typo that leads to a localization error. Depending on the number of minutes, either "minute" or "minutes" should have been displayed. However, in the final condition, the code in the then and else branches is identical, which always outputs "minutes".
As the author of this article says, "Since the text resource for mailbox.minute is already in the game sources, sending the commit took longer than fixing the code." To fix it, developers should delete one letter. Then, the correct word form will be displayed across all languages supported by the mod.
Of course, the error isn't so serious, but because it can be reproduced without any problems, it earns seventh place in our top:
6th place. That was the last straw
Let's move on to the error from AutoMQ:
public static final int MAX_TYPE_NUMBER = 20;
private static final LongAdder[] USAGE_STATS = new LongAdder[MAX_TYPE_NUMBER];
....
public static ByteBuf byteBuffer(int initCapacity, int type) {
try {
if (MEMORY_USAGE_DETECT) {
....
if (type > MAX_TYPE_NUMBER) {
counter = UNKNOWN_USAGE_STATS;
} else {
counter = USAGE_STATS[type]; // <=
....
}
....
}
....
}
}
The PVS-Studio warning: V6025. Possibly index 'type' is out of bounds. ByteBufAlloc.java 151
This is a fairly common error (honestly, so common that the author of this top list has made it very often himself). The array has a size of MAX_TYPE_NUMBER. Due to a minor error in the condition, an OutOfBoundsException may occur on the code line indicated by the analyzer.
It can happen because of if (type > MAX_TYPE_NUMBER). It allows a situation where, in the else branch, the type parameter will have MAX_TYPE_NUMBER, which exceeds the maximum valid array index by one. In this case, an out-of-bounds exception will be thrown, since the very first line accesses the array using the type index.
To fix it, we just need to use >= instead of >.
Since this error is close to the author's heart, it earns sixth place in the top list.
5th place. External data in HTML
Now it's Jetty's turn. Look at the code:
public class HelloSessionServlet extends HttpServlet {
....
@Override
protected void doGet(
HttpServletRequest request,
HttpServletResponse response
) {
....
String greeting = request.getParameter("greeting"); // <=
if (greeting != null)
{
....
message = "New greeting '" + greeting + "' set in session."; // <=
....
}
....
PrintWriter out = response.getWriter();
out.println("<h1>" + greeting + " from HelloSessionServlet</h1>"); // <=
out.println("<p>" + message + "</p>"); // <=
....
}
}
The PVS-Studio warning: V5330. Possible XSS injection. Potentially tainted data in the 'message' variable might be used to execute a malicious script. HelloSessionServlet.java 70
The analyzer points out that there could be an XSS injection.
In the code, we obtain a string from the request, use it to generate messages, and then use those messages to generate an HTML page. Since the request data isn't sanitized in any way, the XSS injection is indeed possible.
If the externally passed message is <script>alert("XSS Injection")</script>, the JavaScript alert("XSS Injection") will be executed when the page is opened. Just like any other js code.
If this is a real vulnerability, why doesn't it rank higher on the list? Because it's a part of a demonstration example. In other words, we show the code just to see what can happen in Jetty. Nevertheless, it's worth remembering that the demonstration example couldn't be copied into production. After all, it's not best practice to show off unsafe code.
As the first taint-analysis warning in a real project, although it's in a demonstration example, we award it 5th place in the top.
4th place. Double only
Look at the error from Elasticsearch.
public static Number truncate(Number n, Number precision) {
....
Double result = (((n.doubleValue() < 0)
? Math.ceil(g)
: Math.floor(g)) / tenAtScale);
return n instanceof Float ? result.floatValue() : result; // <=
}
The PVS-Studio warning: V6088 Result of this expression will be implicitly cast to 'double'. Check if program logic handles it correctly. Maths.java 122
This method cuts the passed number according to the specified precision. For integer values, it reduces the number of digits, while for fractional values, it removes excess digits after the decimal point. The snippet above shows the logic has applied to fractional numbers.
Judging by the ternary operator, in the case of fractions, developers want the method to return an identical number with which it came to the method (either Float or Double). But a subtle but important detail has been overlooked.
The ternary operator is an expression, which means its result must have a single and predetermined type. At this point, the rules of numeric context come into play. In short, the following applies:
- If objects of different types appear in an expression, they're converted to a single common type.
- In this case, the common type would be
Double, since it represents bothDoubleandFloat.
That is, despite expectations, the method always returns a Double when processing fractional numbers.
The compact recording format really let us down. A fix may look like this:
public static Number truncate(Number n, Number precision) {
....
if (n instanceof Float) {
return result.floatValue();
} else {
return result;
}
}
Due to this non-obvious language nuance, the error earns fourth place in the top ten.
3rd place. Please wait your turn!
Now we move on to the award podium. The bronze medalist has been found in IntelliJ IDEA. Look at the code fragment:
public final class UsageType {
....
public static final UsageType
DELEGATE_TO_ANOTHER_INSTANCE_PARAMETERS_CHANGED = new UsageType(
UsageViewBundle.messagePointer(
"usage.type.delegate.to.another.instance.method.parameters.changed"
)
);
private static final Logger LOG = Logger.getInstance(UsageType.class);
public UsageType(@NotNull Supplier<....> nameComputable) {
myNameComputable = nameComputable;
if (ApplicationManager.getApplication().isUnitTestMode()) {
String usageTypeString = myNameComputable.get();
if (usageTypeString.indexOf('{') != -1) {
LOG.error(....);
}
}
}
....
}
The PVS-Studio warning: V6050 Class initialization cycle is present. Initialization of 'DELEGATE_TO_ANOTHER_INSTANCE_PARAMETERS_CHANGED' appears before the initialization of 'LOG'. UsageType.java 46
The analyzer warns about a cyclic dependency during static field initialization; one of those Java nuances that's easy to miss.
The constructor is used to initialize the DELEGATE_TO_ANOTHER_INSTANCE_PARAMETERS_CHANGED field. And we access LOG logger in this constructor.
Fields are initialized in the order in which they're declared. As we can see, DELEGATE_TO_ANOTHER_INSTANCE_PARAMETERS_CHANGED is located above LOG, i.e. it's initialized earlier. As a result, when the constructor runs, LOG will be null, and when we try to access it, we'll encounter an NPE.
As in most of the cases above, the fix will be extremely simple. We just need to place the LOG statement above DELEGATE_TO_ANOTHER_INSTANCE_PARAMETERS_CHANGED.
2nd place. Where did the block go?
We moved on to the silver medalist. Meet again the Minecraft mod, CustomNPC+.
Look at the code fragment:
public int range = 5;
public int speed = 5;
....
public boolean aiShouldExecute() {
healTicks++;
if (healTicks < speed * 10)
return false;
for (Object plObj : npc.worldObj.getEntitiesWithinAABB(
EntityLivingBase.class,
npc.boundingBox.expand(
range,
range / 2, // <=
range))
) {
....
}
healTicks = 0;
return !toHeal.isEmpty();
}
The PVS-Studio warning: V6094 The expression was implicitly cast from 'int' type to 'double' type. Consider utilizing an explicit type cast to avoid the loss of a fractional part. An example: double A = (double)(X) / Y;. JobHealer.java 41
In this code snippet, the game periodically (based on the speed field) scans for entities inside a rectangular area with a radius defined by range, centered around an NPC doctor.
Since the expand method has the expand(double x, double y, double z) signature, the analyzer is absolutely right to warn about this fragment: int is passed to the method where double is expected. "So what?" you may ask. We think this is really strange, so we've decided to investigate this case.
This minor flaw leads to the following consequence: if the radius is an odd number, when evaluating the Y axis, we lose half a block at the top and at the bottom (i.e., a whole block in total). We've proved it by visualizing the collision box calculated from the radius. Look at the visualization:
- The NPC doctor collision is highlighted in white.
- The collision currently implemented (5 / 2) is highlighted in red.
- The collision without integer division (5 / 2.0) is highlighted in green.
As we can see, if we use the 2.0 literal of the double type, the division is no longer an integer, and as a result, the collision will cover exactly the radius we want.
And to confirm that it worked correctly, the author of the article had to "ask the universe":
Notably, developers have fixed this error in the original mod for Minecraft 1.12.2. Unfortunately, I can't provide any direct evidence, so let's say I asked the universe—and this code snippet is what it sent back:
....
this.npc.world.getEntitiesWithinAABB(
EntityLivingBase.class,
npc.getEntityBoundingBox().expand(range, range / 2.0, range)
);
....
Since devs have fixed the bug in the original modification, the same should apply here.
1st place. The mystery case of missed bonus
And now, the gold medal goes to... CustomNPC+! Could the abundance of Minecraft bugs be because half our team spent countless hours working on mods and plugins for it? Who knows.
Let's move on to the code:
....
int startIndex = -1;
boolean number = false;
try {
startIndex = Integer.parseInt(bonusID);
number = true;
} catch (Exception var34) {
number = false;
}
for (startIndex = 0; startIndex < bonuses.length; ++startIndex) {
....
if (number && startIndex == startIndex || // <=
!number && bonusValues[startIndex][0].equals(bonusID)
) {
noNBTText = bonusValues[startIndex][0] + ";" + bonusValueString;
bonuses[startIndex] = "";
bonuses[startIndex] = noNBTText;
....
break;
}
}
....
The PVS-Studio warning:
V6001 There are identical sub-expressions 'startIndex' to the left and to the right of the '==' operator. ScriptDBCPlayer.java 289
V6007 Expression '!number' is always true. ScriptDBCPlayer.java 289
V6033 An item with the same key 'startIndex' has already been changed. ScriptDBCPlayer.java 293
Do warnings really indicate errors? And if so, what does it lead to? Here, a whole detective investigation with an interesting ending awaits us.
First, let's look at the first warning. Comparing startIndex with itself does seem odd. The intrepid author of the article, donning his detective hat and long cloak, goes to look for clues in the class. The evidence is lying right nearby:
File: ScriptDBCPlayer.java(224):
....
int num = -1;
boolean number;
try {
num = Integer.parseInt(bonusID);
number = true;
} catch (Exception var33) {
number = false;
}
for (int i = 0; i < bonuses.length; ++i) {
....
if (number && i == num ||
!number && bonusValues[i][0].equals(bonusID)
) {
bonuses[i] = "";
break;
}
}
....
The code looks very similar, but it's no coincidence that in our team, we call the author of the article "The Keen Eye"—he found the difference! In the second case, the variables are different in the try block and in the for loop: they're num and i. In the first fragment, we first initialize startIndex in the try block, and then iterate through it in a for loop.
Therefore, in the second fragment, two different variables are compared in an identical check: num and i. In the first one, we have what the analyzer pointed out—startIndex == startIndex.
So now we know what the first fragment was supposed to look like.
But what does the error impact?
At this point, the detective noticed an important clue in the name of the class itself: the Script prefix means that the class is used in the game scripting mechanism. The method with error is responsible for assigning bonuses to game attributes such as "Strength", "Agility", etc. Logically, the method is supposed to read a specific bonus index from the in-game script and then apply the corresponding modifier. So how should this have worked?
First, we need to parse the bonus index in the try block:
try {
num = Integer.parseInt(bonusID);
number = true;
} ....
And then, in the loop, set the passed value to the bonus:
for (int i = 0; i < bonuses.length; ++i) {
....
if (number && i == num || ....) {
noNBTText = bonusValues[i][0] + ";" + bonusValueString;
bonuses[i] = "";
bonuses[i] = noNBTText;
....
break;
}
}
But in the fragment with the error, the attribute index obtained from the script is overwritten with zero and then compared with itself:
try {
startIndex = Integer.parseInt(bonusID);
number = true;
} catch (Exception var34) {
number = false;
}
for (startIndex = 0; startIndex < bonuses.length; ++startIndex) {
....
if (number && startIndex == startIndex || // <=
!number && bonusValues[startIndex][0].equals(bonusID)
) {
noNBTText = bonusValues[startIndex][0] + ";" + bonusValueString;
....
}
}
That is, the bonus value passed in the script is always assigned to the first bonus because startIndex is overwritten in the for condition and becomes zero, and then is compared with itself. Now the problem is crystal clear. Let's look at the consequences of the error.
First, we need to write an in-game script that will create bonuses for "Strength".
First, let's create two bonuses with values of 1, and then set their values to 5 and 15, respectively:
This is what in-game scripts look like. We created bonuses for "Strength" with indices 0 and 1, and then assigned them the values.
Now, in theory, if we display these two bonuses from "Strength", we will obtain 5 and 15? If there had been no error in the code, that's what would have happened. However, as we understood from the block above, the error leads to the value of the first bonus being overwritten.
To confirm this behavior, let's write another in-game script that simply prints the bonus values:
As a result, we get:
Indeed, the second bonus in "Power" also has an initial value of 1, while the first bonus now equals 15. It turns out that we first set the first bonus to 5, and then overwrite it with a value of 15. Our hypothesis above turns out to be true. The case has been solved.
Given the amount of detective work involved and the fact that this bug can be reliably reproduced in practice, we confidently award it the honorable first place in our top ten!
Wrap-up
Yay! We've made it through all ten of the most bizarre, heart-warming, and educational bugs, and we hope you enjoyed the journey as much as we did! If you have your own thoughts about this top, or maybe a favorite bug of your own, we'd be happy to see you share them in the comments.
If you want to read more about checking other projects, we invite you on our blog or on this page.
If you feel inspired to check your individual projects and want to try PVS-Studio analyzer yourself, follow the link. And if you're a student, teacher, or maintainer of open-source projects in C, C++, C#, or Java, you can use PVS-Studio for free. All the details are here.
All that's left is to say goodbye for now. Happy Holidays! See ya soon!






Top comments (0)