Exploring AI Code Writing Assistants
Introduction
In today’s world of technology, we observe a significant development in artificial intelligence. Undoubtedly, it will also impact the way software is created and, consequently, developers’ work. There are also numerous concerns about whether artificial intelligence can completely replace programmers. In my opinion, it doesn’t stand a chance in the near future; however, I believe that as programmers, we must continually develop, and thus it is beneficial to know how to use the tools that AI provides us. In this article, I would like to showcase tools that can facilitate and accelerate the software development process. I think that in the context of the near future, individuals who can proficiently use such tools will have a significant advantage in the job market.
Not only Chat-GPT
When most people think of an AI tool that can assist us with various kinds of problems, ChatGPT often comes to mind. However, in the context of software development, there are dedicated tools that strive to facilitate code writing.
Example tools:
- GitHub Copilot https://github.com/features/copilot
- Tabnine https://www.tabnine.com/
- Code Snippets https://codesnippets.ai/
- Cody https://about.sourcegraph.com/cody
- Amazon CodeWhisper https://about.sourcegraph.com/cody
Of course, this list could be much longer. More and more such tools are appearing on the market and, in my opinion, choosing the one that will be comfortable for us to use is a subjective matter. It is also worth paying attention to the legal aspect of using such a tool. If we work with code, e.g., of our client, we cannot decide on our own for it to be processed on servers of external companies who are providers of such a tool.
Before AI Helps You…
Here, I must draw your attention to a critical issue: the legal aspects of using AI tools. Remember, if you are working with client code, you cannot independently decide to have it processed on third-party company servers that provide such tools.
Keep in mind the importance of information security, trade secrets, and personal data. Do not process such data in AI tools unless you are certain that the environment is closed and that the data entered will not be processed or shared with other entities. Verify whether the AI will learn from your prompts and outputs. Before deciding to use AI in your work, consult your internal legal team to review the tool’s licensing terms and provide recommendations on how to safely use the AI solution.
How to start using it?
In this section, I would like to test selected tools and check the benefits we can obtain from them. The tools I decided to test are Tabnine and Cody.
Tabnine
Below, I will show this using the IntelliJ environment as an example.
The simplest of them is just to use the Marketplace in IntelliJ. Where you just need to install the Tabnine plugin, and you can start using it.
We can also visit a specially prepared page:
https://www.tabnine.com/install
Where, after choosing our IDE and following further instructions, we can install the plugin.
Sourcegraph Cody
Cody is another tool I decided to test. To install it, we can also use the sourcegraph website.
https://sourcegraph.com/get-cody
Cody can also be installed not as a plugin but as a separate application. However, in the further part of the article, I will be using the plugin in my IDE.
Similar to Tabnine, the Cody plugin can be installed through the Marketplace in IntelliJ.
Tabnine in action
Tabnine is a tool that allows code autocompletion while writing it. Simply start writing a function or method, and it will suggest its skeleton and part of the implementation. Moreover, by analyzing the context of the code in which we are working, it can propose solutions that are more tailored to our code-writing style.
Upon starting to write a method, it can suggest its body based on the context (the ‘OrderService’ class):
Of course, this is just a suggestion, which we can accept by pressing the ‘TAB’ key, or we can view another hint and switch between them. We can navigate between options using the ‘ALT + [‘ key combination or the ‘option’ key on MacOS. We can also continue to code as usual, and Tabnine will try to suggest the next segment:
Having a model class, we can receive suggestions regarding the next fields that match the given object. For example, below, in the Customer class, after entering the letter ‘p’:
Immediately in this case, phone appears. However, in the Address class, the fields will be things like country or street. What’s important and a significant advantage of Tabnine is that this tool adapts to our coding style. For instance, if we have a builder pattern written and we’re adding more fields, we do it practically effortlessly while maintaining our own pattern.
I’ve noticed that Tabnine can be very helpful when writing tests because it can automatically complete known test data of a certain type:
Another example: This time Tabnine understood what I wanted to do based on the method name:
In my opinion, this is very helpful, although while using this tool, sometimes certain things were not considered. As in the example below, it forgot to add ‘asList()’:
It can be observed that Tabnine is learning quite well. For example, initially, Tabnine always used the Arrays class to create a list.
However, when the ‘List.of’ construction is used instead of the suggestion, Tabnine starts using it.
Summary for Tabnine
Personally, after testing Tabnine, I find it to be a very helpful tool with a big potential. It’s possible to notice errors or misunderstandings in some suggestions, but it’s also evident that Tabnine learns our coding style. The suggestions are not intrusive, and it’s easy to press the TAB key to use the provided piece of code. This is especially useful in cases where we need to create some repetitive code, which can be significantly sped up and simplified thanks to this tool.
Working with Cody
Cody is also a very promising tool that provides auto-completion similar to Tabnine. Cody is a product of Sourcegraph, and if a company uses this tool, Cody is even more likely to suggest solutions already used within the organization.
To avoid repeating the description of how auto-completion works when introducing Cody, I will focus on the additional functionalities available here.
After successful installation in our IDE, a new icon should appear, giving us access to a chat and predefined options visible below.
Besides, these predefined options, we can converse in natural language with ‘Cody’ about our code. Let’s assume we have a simple OrderService class that looks like this:
@Service
public class OrderService {
public Order createOrder(Customer c, List<Product> products) {
return OrderBuilder.anOrder()
.withCustomer(c)
.withOrderDate(new Date())
.withProducts(products)
.withTotalPrice(calculateTotalPrice(products))
.build();
}
private double calculateTotalPrice(List<Product> products) {
return products.stream()
.mapToDouble(product -> product.getPrice() * product.getQuantity())
.sum();
}
}
Using the ‘Smell code’ function, we can check our code for:
- Usage of Date and SimpleDateFormat classes can be error-prone – consider using java.time instead
- Potential for NullPointerExceptions (NPEs) if input parameters are not validated
- Large methods that could potentially be broken down into smaller pieces
- Duplicated logic that could be extracted into reusable methods
- Usage of older Java language features like loops instead of streams
- Potential concurrency issues if code is multi-threaded
- Proper exception handling
In the context of the above code, the result is:
1. Direct use of java.util.Date can be problematic due to mutability and error prone.
2. No checking of input parameters "c" and "products" for null values.
This can lead to NullPointerException. Verify the required parameters.
3. calculateTotalPrice calculates the total amount with each call.
You can store the total price in the Order object during creation to avoid recalculation.
4. The OrderBuilder pattern is correct, but it comes at the cost of creating many small intermediate objects.
Optimization could be done based on usage.
5. No logging or error handling. Exceptions in stream operations may go unnoticed.
6. Lack of testing. Adding unit tests would help catch errors and prevent regressions.
When Cody was asked to change variable names, it introduced the following changes:
- in 'createOrder' method c was changed to customer
- in 'calculateTotalPrice' method products changed to orderProducts
The ‘Explain Selected Code’ option appears to be a very useful feature. However, I suspect its utility will increase with the complexity of the method and a decrease in its readability. I introduced a bit of complexity to the ‘calculateTotalPrice’ method to see how it would be explained.
private double calculateTotalPrice(List<Product> products, String code) {
Discount discount = discountRepository.findByCode(code);
Double totalPrice = products.stream()
.mapToDouble(product -> product.getPrice() * product.getQuantity())
.sum();
return discount == null
? totalPrice
: applyDiscount(totalPrice, discount);
}
private double applyDiscount(Double totalPrice, Discount discount) {
return (100 - discount.getPercentOfDiscount()) * totalPrice / 100;
}
Here is the generated explanation for this method:
1. calculateTotalPrice method accepts List<Product> products and String code as parameters.
2. first calls discountRepository.findByCode(code) to find a Discount object based on the given code. This checks for any possible discounts for the order based on the code.
3 It calculates the total price without any discounts by:
- Streaming (passing) through a list of products
- Mapping each Product to its total price by multiplying price * quantity
- Summing the mapped stream to get the total price
4 The result is stored in a Double variable called totalPrice. This is the price before the discount is applied.
5. checks if the discount is null, which means that no discount was found.
6. if it is null, simply returns totalPrice without applying any discount.
7. otherwise, calls the applyDiscount method, passing totalPrice and the discount object.
8. applyDiscount calculates the price after discount as follows:
- Takes 100 - discount.getPercentOfDiscount() to get the percentage to keep after the discount
- Multiplies this percentage by totalPrice
- Divides by 100 to get the final price after discount
9 The post-discount price is returned from applyDiscount.
It seems to me that such analysis can be helpful in pinpointing errors, where programmers often spend a lot of valuable time, especially when dealing with low-quality code that they didn’t author themselves.
The next feature worth mentioning is the ability to generate a test. To verify how Cody would handle generating a test, for a function containing a condition I modified the ‘createOrder’ method as follows:
public Order createOrder(Customer c, List<Product> products) throws CountryNotSupportedYetException {
if (isCustomerFromPoland(c)) {
throw new CountryNotSupportedYetException();
}
return OrderBuilder.anOrder()
.withCustomer(c)
.withOrderDate(new Date())
.withProducts(products)
.withTotalPrice(calculateTotalPrice(products), c)
.build();
}
The generated class:
import org.example.model.Customer;
import org.example.model.Product;
import org.example.service.CountryNotSupportedYetException;
import org.example.service.OrderService;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import java.util.Arrays;
import java.util.List;
public class OrderServiceTest {
private OrderService orderService = new OrderService(null);
@Test
public void testCreateValidOrder() {
// Arrange
Customer customer = new Customer("John", "123 Main St");
Product product = new Product("Apple", 10, 2);
List<Product> products = Arrays.asList(product);
// Act
orderService.createOrder(customer, products);
// Assert
// No exception thrown
}
@Test
public void testCreateOrderUnsupportedCountry() {
// Arrange
Customer customer = new Customer("John", "123 Main St, Poland");
List<Product> products = Arrays.asList();
// Act and Assert
Assertions.assertThrows(CountryNotSupportedYetException.class, () -> {
orderService.createOrder(customer, products);
});
}
@Test
public void testCalculateTotalPrice() {
// Arrange
Product product1 = new Product("Apple", 10, 2);
Product product2 = new Product("Orange", 5, 3);
List<Product> products = Arrays.asList(product1, product2);
// Act
double totalPrice = orderService.calculateTotalPrice(products);
// Assert
Assertions.assertEquals(35, totalPrice);
}
}
A couple of mistakes were made here, and the class needs to be corrected and modified after copying. The main problems are, for example, that non-existent constructors were used, and also the test includes a private method, which cannot be tested in such a way. However, in my opinion, if we are to write tests for a class then such a generated class can be a pretty good starting point, speeding up the writing of test classes.
Of course, we can also try to guide the artificial intelligence in the expected direction. For example, when JUnit assertions have been used and I would prefer to get assertions from the AssertJ package in my tests.
I can ask for the following assertions:
Could you use AssertJ assertions with single and joined assertion?
And get an assertion:
// Assert
Assertions.assertThat(order)
.hasFieldOrPropertyWithValue("customer", customer)
.hasFieldOrPropertyWithValue("orderDate", new Date())
.hasFieldOrPropertyWithValue("products", products)
.hasFieldOrPropertyWithValue("totalPrice", 10.5);
When it comes to Cody and processing queries about our code, we can really use a lot of options here. We can try to find a bug through conversation, ask about possible improvements or potential threats.
As an example:
Which country is not supported and could lead to CountryNotSupportedYetException?
As an example:
Based on the code snippet provided, the country "Poland" is not supported yet and could lead to a CountryNotSupportedYetException being thrown:
java
if (c.getAddress().getCountry().equals("Poland")) {
throw new CountryNotSupportedYetException();
}
The code is checking if the customer's address country is "Poland". If so, it will throw the CountryNotSupportedYetException.
So Poland is the country that is not supported yet that could result in that exception.
Of course, in such a simple project we might as well search for the code and analyze it ourselves, but if we were talking about an error in a much more complex project. I think then that in this way we would sometimes be more efficient to get to the root cause of a potential problem and thus save a lot of valuable time.
Summary
I believe I’ve managed to showcase how such tools work in this article. Personally, I see a significant potential in the development of such solutions. However, it’s important to remember that their primary goal is to facilitate and accelerate the code-writing process rather than take it over completely. The suggestions offered by the tested solutions were not flawless, so control over them is crucial. As these tools become more popular, they are likely to improve. Currently, I believe they have the ability to alleviate the daily workload associated with repetitive code and can also aid in developers’ growth, mainly through error analysis and providing tips on how to improve certain aspects. After all, it’s known that sometimes a human may not anticipate all possible issues during the code-writing stage.