Code Wizardry: Casting Spells to Fix Bugs

Andres Sacco

introduction

Multiple developers may introduce changes to develop new features or fix bugs during an application’s life. At the same time, these changes could introduce problems in the code, such as bad practices regarding the declaration of attributes, duplicate code, or pieces of code with a high risk of producing errors. 

In many cases, this situation could be detected on a peer review of the code for another developer but implies that someone needs to spend too much time focusing on small details instead of focusing on the problem that the code tries to fix. The appearance of tools like PMD, Checkstyle, SonarLint, and many others that detect problems analyzing the code mitigates most of the problem, but let’s see in detail the impact on the day-to-day of the company or developer.

Impact of Bugs and Bad Practices

Harmful practices could result in many problems in the application, but not all of them appear together. Each developer may introduce a slight code smell or problem, such as the way to declare variables or not consider the possibility of null values in some attributes, where no one will detect it because it is a part of the application that is not frequently used. Still, when many developers do the same for an extended period, the problem will be visible and have a huge impact, causing all the developers to spend many hours trying to understand why this happened.

Common Consequences

Introduce a series of poor practices, and the impact could trigger a cascade of unintended consequences, including memory leaks that gradually affect the application’s performance by consuming more and more resources until the application crashes, experiences unpredictable behavior, or even becomes vulnerable to security threats. Additionally, performance bottlenecks could turn features into nightmares, while logic errors could lead to incorrect data processing or cause the application to break.

Reduced Maintainability

When the code is infested with bugs, it causes problems for the application and makes it significantly more challenging to maintain. Some issues that impact maintainability include spaghetti code, cryptic error handling, and poorly documented fixes, which diminish the developer’s understanding of why the code is structured that way.

Imagine an application where a bug related to resource synchronization was fixed with a random Thread.sleep() to prevent race conditions. The problem looks to be solved initially, but when the application grows and processes more information, a new issue appears about the time the thread sleeps.

public class Counter {
    private int count = 0;

    public void increment() {
        try {
            Thread.sleep(100); // Temporary "fix" 
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        count++;
    }

    public int getCount() {
        return count;
    }
}

Another problem could be not correctly tackling the NullPointerException, such as encapsulating in a try/catch block instead of using another structure like Optional.

public class UserService {
    public String getUserDisplayName(User user) {
        try {
            return user.getName().toUpperCase(); // It could throw NullPointerException
        } catch (NullPointerException e) {
            return "Unknown User"; // Hides real problem
        }
    }
}

One last example could be a bad comparison in the enumeration to do something depending on the value; in some cases, instead of using the enumeration methods or comparing with the value, some developers invoke a toString() method and compare two different strings.

enum LogLevel {
    INFO, DEBUG, ERROR;
}

public class Logger {
   public void log(LogLevel level, String message) {
        // BAD: Using toString() for comparison
        if (level.toString().equals("DEBUG")) {
            System.out.println("DEBUG: " + message);
        }
        if (level.toString().equals("ERROR")) {
            System.err.println("ERROR: " + message);
        }
        if (level.toString().equals("INFO")) {
            System.out.println("INFO: " + message);
        }
    }
}

Many problems are not mentioned, but they all have the same consequences for the application: they reduce the code’s readability and maintainability.

Performance Issues

Performance issues are often connected with maintainability when someone fixes a problem inefficiently, and the code remains in production for a long time. For example, instead of using StringBuilder’s append method to concatenate a list of strings, someone could consider using the add operation, which produces a new object each time the application uses its resources.

import java.util.List;

public class StringExample {
    public String buildString(List<String> items) {
        String result = "";
        for (String item : items) {
            result += item;  // Inefficient because it creates a new String each time
        }
        return result;
    }
}

In another case, someone tries to obtain more information about a particular situation on a block of code, which creates random results, so decides to introduce a lot of logs or use System.out.println(), incurring at some point in performance overhead.

public class PaymentProcessor {

    // This debug flag should be removed in production
    private static final boolean DEBUG_MODE = true;

    public void processPayment(double amount) {
        // Debug logging that incurs performance overhead
        if (DEBUG_MODE) {
            System.out.println("DEBUG: Starting payment process for amount: " + amount);
        }
       // More code to do something
    }
}

Some of these problems could be obvious to the experienced developer, but not in all cases. Let’s consider the scenario where someone creates an MR with 50 files with changes, which in most cases are not just two lines on each file, so the complexity of paying attention to all of them and detecting each possible problem is low.

Highlight the Causes

When bugs appear, it’s not enough to address the symptoms and understand how to fix them—it’s necessary to find the root causes of those problems, not just patch the code and continue because, in that case, the problem will continue happening.

Sometimes, developers lacking experience are unaware of subtle issues, or unrealistic deadlines force the team to implement quick fixes instead of addressing the real problem. It could be just one reason or a combination, but whatever the root cause, the solution is ignored or delayed, producing recurrent issues.

Lack of Experience

In most cases, the experience and level of the developers are factors that could increase or decrease the number of bugs and the possibility of detecting those problems in a review process. Another thing to consider is how many of your company’s developers know about the feature in the latest version of Java that your company uses; this is especially useful because many things change across different versions, like the appearance of pattern matching or new ways to declare variables. It does not make sense for your company to use a static analysis tool to detect problems if the developers do not understand the behind-the-scenes issues.

The following is a possible example of a harmful use of a new feature that was included in many versions ago.

// Definition of a record, a feature introduced in recent versions of Java.
public record Person(String name, int age) { }

public class RecordExample {
    public static void main(String[] args) {
        Person person1 = new Person("Alice", 30);
        Person person2 = new Person("Alice", 30);

        // BAD USAGE: Using '==' to compare two records.
        if (person1 == person2) {
            System.out.println("The persons are equal (using '==').");
        } else {
            System.out.println("The persons are different (using '==').");
        }
    }
}

The error is quite simple, but not in all cases. It is not trivial because the application does not have just a few lines of code.

Time Constraints

The deadlines for launching a new application or feature or solving a bug decrease code quality because there is no time to check everything. Most developers focus on the happy path or just a few cases to check if nothing terrible occurs. This approach of not paying attention to the quality of the code is like a bomb that could explode shortly because not all lousy situations arise at the beginning. A flow may depend on a series of conditions that are not frequent.

The worst scenario is that your company does not have at least one static analysis tool to detect and track the problems. If the deadline reduces the chance to check and fix everything, at least you have a tool to help you when the issues appear instead of finding yourself across all the code problems.

The Complexity of Modern Applications

The evolution of application design, such as the use of hexagonal instead of layer architecture, the use of too many libraries or frameworks to simplify some repetitive tasks, and changes in the mechanism for interacting with databases, such as Spring Data or another library, or communicating with other applications, such as synchronous communication or using events, increases the complexity and the number of things that developers need to know. 

This situation exacerbates the problem because the developers must pay attention to many small details to take action. This is not a problem, but without a correct peer review process or the use of a tool to detect it, most of them pass through the developers’ noise, and no one sees them.

Code Reviews or Automated Checks

Another big problem affecting the quality of the code is the lack of a proper validation process. Some companies delegate everything on each Merge Request to a developer, such as checking whether the code senses the feature description and has the correct number of tests, units, or integration.

This type of approach, in most cases, produces big problems because anyone can check everything all the time without making a mistake, and the risk when someone does something is zero. Still, there are ways to mitigate, like a combination of developers who check whether the code makes sense and delegate the analysis to a tool. This situation is the best scenario if your company’s tool can detect and fix problems without intervention. Still, as a suggestion, everyone needs to understand why the tool changes the code because it will reappear if no one understands the situation.

Static Code Analysis Tools

These tools play a crucial role in software development. They scan the code to detect fragments that do not adhere to coding standards, identify potential bugs, and suggest solutions for these problems. Developers can use these tools to help identify issues early in development. The tools can be run locally before pushing the code into the repository. This practice reduces the time and effort needed to fix problems after they are detected, especially when they appear on a website as reports of the issues.

Tools

There are too many tools related to code analysis, but this subsection focuses on three popular ones: PMD, Checkstyle, and SonarLint. Each tool offers unique benefits: PMD scans for bugs and inefficiencies, Checkstyle enforces style and coding conventions, and SonarLint provides real-time feedback directly within your development environment. 

Let’s look at each tool in more detail to understand its scope and benefits to developers.

ToolFeatures
SonarLint– Provide real-time feedback directly in your IDE
– Have integration with SonarCloud/SonarQube so it’s possible to use the same set of rules locally rather than remotely.
– Quick detection of vulnerabilities and bugs in the code, offering a description of how to fix them and the reasons why is a good practice in most cases.
Checkstyle– Enforce code standards and style conventions, check the code, and show the existing problems.
– It’s one of the tools that offers more customization of the rules and messages about issues.
– It can be used inside your project with Maven/Gradle or as a plugin in your IDE.
PMD– Detect a great range of problems like bugs and code smells.
– Support the creation of custom rules to detect or check specific things on the code.
Table – Key features of the most relevant tools in the static code analysis

Other tools exist, such as Spotbugs, Huntbugs, and Spoon, but most do the same thing as the three previous tools mentioned in the first paragraph. They all have pros and cons; no silver bullet will solve all your problems.

Limitations of These Tools

All the static code analysis tools are incredible and invaluable for detecting a broad range of issues in the early development steps, but they all have limitations at some point. Primarily, they all identify problems and show why some of the code is incorrect but cannot automatically fix them, requiring one or more developers to manually review and address the flagged matters. 

Resolving these problems implies considerable time and effort, especially if you start using these tools with large or legacy applications with many issues. Not all developers have to check an extensive list of problems, understand them, and try to fix each.

Another problem is recurrence, the number of times that the same problem appears in the code, which implies applying many times to introduce the same fix; in some cases, it is just changing one line in the code, or, in another case, it is more complex but in the simple cases could be tedious do the same corrections many times.

Automating Fixes

The next level of the static analysis code is not just detecting the problems; in some cases, the issue is simple, just fixed without human intervention. Of course, in the beginning, someone needs to configure the tools as the others do, but the interventions tend to be zero afterward. One of the most prominent exponents of these types of tools is error-prone.

Introduction to the Error Prone

Error Prone is an open-source static analysis tool developed by Google. It integrates directly with the Java Compiler using Maven or Gradle or as a plugin in the IDE. The tool hooks into the compile-time process, inspecting the code being compiled simultaneously to detect potential bugs or pitfalls before proceeding. This proactive approach catches issues overlooked during traditional testing or code reviews.

In addition to identifying problems, the tool sometimes suggests fixes or can automatically apply corrections to your code without your intervention. To achieve this, one core library contains the most relevant rules related to Java and how to fix them, while other companies, like Picnic, create their own set of rules, which can be combined with those defined by Google.

Unlike traditional tools, which operate as a separate step in the development workflow during the code review process or as part of continuous integration execution, Error Prone reduces context switching and provides quick feedback to developers regarding issues. Furthermore, it identifies complex problems that most conventional tools may miss and offers a detailed analysis level that enhances your code’s reliability.

Benefits of Using Error Prone

Using error_prone brings multiple benefits, including early detection of complex issues, automated fixes, and immediate feedback that helps enforce best practices and boost productivity. Let’s see more in detail the most relevant:

  • Detecting complex issues: The tool automatically detects problems related to harmful practices or performance issues without writing extra rules. However, it’s possible to include an external set of rules or create relevant new rules. It could be a good idea to analyze which companies make their own rules instead of building one from scratch.

  • Auto-fixing specific problems: This benefit is one of the most significant differences from other tools because it minimizes the manual effort needed to resolve common issues. You can configure the tool to display the problems and use additional commands to indicate whether to fix them or to perform both actions simultaneously. It’s advisable to separate the process into two distinct steps or commands until you fully understand the rules and which ones are appropriate to enable. After that, you can use a single command to conduct the entire process.

  • Enhanced Developer Productivity Through Immediate Feedback: Receiving real-time feedback on code enables developers to avoid spending hours identifying issues in a code block and minimizes the time other developers must dedicate to reviewing each line of code.

  • Enforcing Best Practices and Avoiding Common Pitfalls: This tool highlights issues in the code and identifies the parts that stray from established best practices. When developers receive prompt feedback on code quality, they are less likely to make the same mistakes in the future, motivating them to write more robust and maintainable code.

Example of Use

Let’s begin with an example of how this tool can be used in a project that may encounter quality-related issues. The project’s source code, which will be used to practice everything, is in this repository on GitHub.

The first step is to add the dependency related to error_prone, found in the Maven Repository, to the application’s configuration. The application uses Maven, but it could be the same on Gradle.

<build>
   <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <version>3.11.0</version>
            <configuration>
                <source>21</source>
                <target>21</target>
                <encoding>UTF-8</encoding>
                <compilerArgs>
                    <arg>-XDcompilePolicy=simple</arg>


                    <!-- Enable error_prone as a plugin -->
                    <arg>-Xplugin:ErrorProne</arg>
                </compilerArgs>
                <annotationProcessorPaths>
                    <path>
                        <groupId>com.google.errorprone</groupId>
                        <artifactId>error_prone_core</artifactId>
                        <version>2.30.0</version>
                    </path>
                </annotationProcessorPaths>
            </configuration>
        </plugin>
    </plugins>
</build>

By adding this simple configuration to the maven-compiler-plugin, your project can use error_prone. If you want to include external libraries with extra rules, you should include them on the section path after or before error_prone_core.

There is a problem if your project uses Java 16 or up because there is a strong encapsulation, so running any command on Maven with those changes will fail. A way to fix this problem is to add a file on .mvn/ with the name jvm.config and add flags like the following:

--add-exports jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED
--add-exports jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED
--add-exports jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED
--add-exports jdk.compiler/com.sun.tools.javac.model=ALL-UNNAMED
--add-exports jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED
--add-exports jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED
--add-exports jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED
--add-exports jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED
--add-opens jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED
--add-opens jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED

Alternatively, it’s possible to introduce the same configuration into the pom file. By making these changes to the application and executing the simple command mvn clean verify, all possible bugs or problems in the code will be checked by default. The entire list of checks for this tool is available at this link.

Let’s examine some common errors that might occur in the source code of any project. If it’s necessary to throw an exception when a record does not exist in the database, instead of doing it correctly, your code looks like this:

   public ReservationDTO getReservationById(Long id) {
        Optional<Reservation> result = repository.getReservationById(id);
        if (result.isEmpty()) {
            LOGGER.debug("Not exist reservation with the id {}", id);
            new TWAException(APIError.RESERVATION_NOT_FOUND);
        }
        return conversionService.convert(result.get(), ReservationDTO.class);
    }

The following output will appear on the console, indicating that something is wrong and which could be the possible solution.

[ERROR] COMPILATION ERROR : 
[INFO] -------------------------------------------------------------
[ERROR] /home/asacco/code-wizardry/api-reservations/src/main/java/com/twa/reservations/service/ReservationService.java:[48,13] [DeadException] Exception created but not thrown
    (see https://errorprone.info/bugpattern/DeadException)
  Did you mean 'throw new TWAException(APIError.RESERVATION_NOT_FOUND);'?

Not all problems can have the same severity; for example, imagine that you need to create an equals method on the entity class like the following block.

public class Price {

    private Long id;
    private BigDecimal totalPrice;

    private BigDecimal totalTax;

    private BigDecimal basePrice;

    // Setters and Getters


    @Override
    public boolean equals(Object o) {
        if (this == o)
            return true;
        if (o == null || getClass() != o.getClass())
            return false;
        Price price = (Price) o;
        return Objects.equals(id, price.id) && Objects.equals(totalPrice, price.totalPrice)
                && Objects.equals(totalTax, price.totalTax) && Objects.equals(basePrice, price.basePrice);
    }
}

If you execute the command again to verify the code, the console does something like the following block:

[WARNING] /home/asacco/code-wizardry/api-reservations/src/main/java/com/twa/reservations/model/Price.java:[48,20] [EqualsGetClass] Prefer instanceof to getClass when implementing Object#equals.
    (see https://errorprone.info/bugpattern/EqualsGetClass)
  Did you mean 'if (!(o instanceof Price))'?

The basic rules can detect many other things, but this article does not explain all of them.

Running error_prone auto-fixes

Detecting bugs and providing information on how to fix them is necessary. However, in some cases where the problem is trivial, the plugin should resolve this issue without any intervention. To achieve this, it’s essential to modify the plugin’s configuration by specifying what tasks it should handle to fix the issue. Let’s make some adjustments to address all occurrences of the problems mentioned in the previous section; replace the last line that enables error_prone with the following:

<arg>-Xplugin:ErrorProne -XepPatchChecks:DeadException,EqualsGetClass -XepPatchLocation:IN_PLACE</arg>

After that, when any command is executed, all the problems will fixed automatically without any intervention. Of course, it’s possible to indicate that solve all of the issues, not just a few of them, but as a suggestion, always try to identify which problems are good candidates to solve automatically and which of them it’s okay to have a report or notification about the issue.

Advanced Configuration

Not all issues require the tool’s default configuration; in some cases, changing the severity of a type of issue or just disabling it because it’s irrelevant to your application is essential. Error_prone offers the possibility of having a granular configuration considering all these possible scenarios.

The following table refers to some of the most common parameters:

ParameterDescription
-XepAllErrorsAsWarningsConverts all issues that the library could detect as errors into warnings. This could be useful for compiling everything, but at the same time, we need to know which problems are in the code.
-XepAllSuggestionsAsWarningsDowngrades all suggestion messages to warnings. 
-XepAllDisabledChecksAsWarningsInstead of completely ignoring checks that have been disabled, this flag treats them as warnings. 
-XepDisableAllChecksDisable all the checks of the library.  This could be useful to bypass all the validations of the tool temporarily.
-XepDisableAllWarningsSuppresses all warning messages from the tool so no warnings are shown during compilation.
-XepDisableWarningsInGeneratedCodeDisable all the library checks. This could be useful temporarily to bypass all the tool’s validations.
Table – Default parameters to use on the configuration

In addition to the previous parameters, it’s possible to change the severity or disable/enable some specific rules. Let’s see in the following table how to do it with DeadException.

ParameterDescription
-Xep:DeadExceptionThis parameter turns on the validation with a severity that the rule has by default.
-Xep:DeadException:OFFThis parameter turns off the validation.
-Xep:DeadException:WARNWith this parameter, it’s possible to change the severity of the rule to WARN.
-Xep:DeadException:ERRORWith this parameter, it’s possible to change the severity of the rule to ERROR.
Table – Custom parameters to modify the behavior of the different rules

With all of these options in the tool’s configuration, you can achieve an excellent level of validation and recommend what is relevant to your project.

Using External Libraries of Rules

Sometimes, the rules offered by default error_prone are insufficient or do not cover all the scenarios in the typical applications. To solve that, it’s likely to add some extensions that add new rules; one of the most famous is Pinic’s library, which offers a wide range of regulations related to the use of loggers or the ways to use some variables.

To use this extension on the rules, it’s necessary to  add two new dependencies in the same place where it is located error_prone_core, like appears on the following fragment:

<annotationProcessorPaths>
    <!-- Error Prone itself. -->
    <path>
        <groupId>com.google.errorprone</groupId>
        <artifactId>error_prone_core</artifactId>
<version>${error-prone.version}</version>
    </path>
    <!-- Error Prone Support's additional bug checkers. -->
    <path>
        <groupId>tech.picnic.error-prone-support</groupId>
        <artifactId>error-prone-contrib</artifactId>
<version>${error-prone-support.version}</version>
    </path>
    <!-- Error Prone Support's Refaster rules. -->
    <path>
        <groupId>tech.picnic.error-prone-support</groupId>
        <artifactId>refaster-runner</artifactId>
<version>${error-prone-support.version}</version>
    </path>
</annotationProcessorPaths>

It’s essential to check the compatibility of the versions on Picnic’s error_prone with the core listed in the official documentation.

After that, if the compilation process occurs again, it’s possible to see new errors that appear in the following output:

[INFO] /home/asacco/Code/code-wizardry/api-reservations/src/test/java/com/twa/reservations/util/ReservationUtil.java:[43,37] [Refaster Rule] ImmutableListRules.ImmutableListOf1: Refactoring opportunity
    (see https://error-prone.picnic.tech/refasterrules/ImmutableListRules#ImmutableListOf1)
  Did you mean 'itinerary.setSegment(ImmutableList.of(segment));'?


[WARNING] /home/asacco/Code/code-wizardry/api-reservations/src/test/java/com/twa/reservations/service/ReservationServiceTest.java:[26,33] [Slf4jLoggerDeclaration] SLF4J logger declarations should follow established best-practices
    (see https://error-prone.picnic.tech/bugpatterns/Slf4jLoggerDeclaration)
  Did you mean 'private static final Logger LOG = LoggerFactory.getLogger(ReservationServiceTest.class);'?

Reducing the scope or the number of enabled rules is relevant in the same way that it’s necessary to do with the basic rules.

What’s Next?

There are many resources on different topics connected with the quality of the applications and how bugs affect an application. Still, just a few tackle solving or mitigating it using static code analysis. The following is just a short list of resources:

Other resources that could be great for understanding some concepts related to good practices and the relevance of the quality of an application are:

This is just a small list of all the available resources on quality and static analysis. If something is unclear, find another video, book, or resource.

CONCLUSION

Problems related to lousy code quality are ineligible; they could appear at the beginning of an application or take time, depending on many aspects, such as the developer’s experience, the time they have to review, and their focus on each little detail. 

One of the most effective ways to mitigate or reduce the problem is to use static analysis tools. Of course, tools like error_prone will help the developer detect the issue because, often, this tool could fix it. This tool, like others, allows developers to understand where a problem is and improve their ability to identify incorrect things, so consider using one in your project or company.

Total
0
Shares
Previous Post

Launching a Java Debugger in Eclipse – A Myriad of Options

Next Post

Improving Platform Observability with Distributed Tracing and OpenTelemetry

Related Posts