Blog AI/ML Refactor code into modern languages with AI-powered GitLab Duo
Published on: August 26, 2024
21 min read

Refactor code into modern languages with AI-powered GitLab Duo

This detailed tutorial helps developers use AI to modernize code by switching to a new programming language and gain knowledge about new features in the same language.

GitLab Duo Workflow 2 - cover

Whether you are tasked with modernizing the code base or framework by switching to a new programming language, or you need knowledge about new language features in the same language, AI-powered GitLab Duo can help. Learn how to approach code refactoring challenges with best practices using examples from the past 20 years of my coding career.

The prompts and examples in this article are shown in different IDEs: VS Code and JetBrains IDEs (IntelliJ IDEA, PyCharm, and CLion) with the GitLab Duo extensions/plugins installed. The development environment uses GitLab.com, including updates to Anthropic Claude 3.5 as Large Language Model (LLM) for GitLab Duo Code Suggestions and Chat. Spoiler: They are even more powerful and efficient.

You can navigate into each section of the article, or read top-down. The source code and challenges with exercises are provided for self-learning, too.

Refactor code to modern programming language standards

The next sections show how to refactor, explain, and understand the language specific differences in a defined programming language.

Generate Java 7 and refactor to Java 8

A customer wanted to know if GitLab Duo supports Java 7. While testing the Code Suggestions and Chat prompts to generate Java 7 specific code, I also thought of migration help. Anthropic Claude 3.5 is capable of understanding the language-specific differences, and so is GitLab Duo.

The following example shows a prompt to generate Java 7 code with Code Suggestions.

// Generate a new class for a File Manager. Use Java 7 features only.
import java.io.*;
import java.nio.file.*;
import java.util.*;

public class FileManager {
    private Path rootDirectory;

    public FileManager(String rootPath) throws IOException {
        this.rootDirectory = Paths.get(rootPath);
        if (!Files.exists(rootDirectory)) {
            Files.createDirectories(rootDirectory);
        }
    }

    public void createFile(String fileName, String content) throws IOException {
        Path filePath = rootDirectory.resolve(fileName);
        Files.write(filePath, content.getBytes(), StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
    }

    public String readFile(String fileName) throws IOException {
        Path filePath = rootDirectory.resolve(fileName);
        return new String(Files.readAllBytes(filePath));
    }

    public void deleteFile(String fileName) throws IOException {
        Path filePath = rootDirectory.resolve(fileName);
        Files.delete(filePath);
    }

    public List<String> listFiles() throws IOException {
        List<String> fileList = new ArrayList<>();
        try (DirectoryStream<Path> stream = Files.newDirectoryStream(rootDirectory)) {
            for (Path file : stream) {
                fileList.add(file.getFileName().toString());
            }
        }
        return fileList;
    }

    public void copyFile(String sourceFileName, String destinationFileName) throws IOException {
        Path sourcePath = rootDirectory.resolve(sourceFileName);
        Path destinationPath = rootDirectory.resolve(destinationFileName);
        Files.copy(sourcePath, destinationPath, StandardCopyOption.REPLACE_EXISTING);
    }

    public void moveFile(String sourceFileName, String destinationFileName) throws IOException {
        Path sourcePath = rootDirectory.resolve(sourceFileName);
        Path destinationPath = rootDirectory.resolve(destinationFileName);
        Files.move(sourcePath, destinationPath, StandardCopyOption.REPLACE_EXISTING);
    }

    public long getFileSize(String fileName) throws IOException {
        Path filePath = rootDirectory.resolve(fileName);
        return Files.size(filePath);
    }

    public boolean fileExists(String fileName) {
        Path filePath = rootDirectory.resolve(fileName);
        return Files.exists(filePath);
    }
}

You can continue with the following steps in your IDE:

  1. Learn and understand the generated code: Select the code in the editor (blue color in the screenshot), switch into GitLab Duo Chat and send the /explain slash command as prompt.

Validate the generated code with the  slash command in GitLab Duo Chat

  1. Refactor the Java 7 code into Java 8: Select the code in the editor, switch into Chat, and send /refactor using Java 8 features as refined prompt.
  2. Practice an alternative to refactoring: Create a new file java8.java and generate Java 8 specific code using Code Suggestions with the prompt // Generate a new class for a File Manager. Use Java 8 features only..
  3. Continue writing Java 8 code in the same context with AI-powered code completions.

You can watch all steps in this recording.

The source code is available in the GitLab Duo Challenge - Generate and refactor Java 7 to 8 project.

Refactor across C++ standards

The C++ standard has a long history and can be considered stable and mature. Depending on the target architecture and platforms, the supported standard depends on the compiler on the host operating system.

In my career, I needed to support 25 different Linux and Windows distributions in open source projects, and have seen varying compiler support for newer language features and C++ standards. In 2015, we settled on C++11 features, and later never touched based on C++14 or C++17.

Refactoring functions and algorithms to a new C++ standard would have required too much effort, and help explaining the differences was needed. Back then, we researched through books, online search, Stack Overflow, and sometimes tools assistance for refactoring.

Things have changed with AI-powered assistants and GitLab Duo: You can instruct Code Suggestions and Chat to provide source code in a specific C++ standard. Then, it is also possible to compare the different outputs.

The following example uses a prompt to specifically generate source code for the C++03 standard. It implements a simple config parser for the .ini configuration format.

/* Create a config parser which can parse an ini format
Use C++03 standard only
 */

#include <iostream>
#include <fstream>
#include <string>
#include <map>
#include <sstream>

class ConfigParser {
private:
    std::map<std::string, std::map<std::string, std::string> > config;

public:
    ConfigParser() {}

    bool parse(const std::string& filename) {
        std::ifstream file(filename.c_str());
        if (!file.is_open()) {
            return false;
        }

        std::string line;
        std::string currentSection;

        while (std::getline(file, line)) {
            // Trim whitespace
            size_t first = line.find_first_not_of(" \t");
            size_t last = line.find_last_not_of(" \t");
            if (first == std::string::npos) continue; // Skip empty lines
            line = line.substr(first, (last - first + 1));

            if (line[0] == '[' && line[line.length() - 1] == ']') {
                // This is a section
                currentSection = line.substr(1, line.length() - 2);
            } else {
                // This is a key-value pair
                size_t delimiterPos = line.find('=');
                if (delimiterPos != std::string::npos) {
                    std::string key = line.substr(0, delimiterPos);
                    std::string value = line.substr(delimiterPos + 1);
                    config[currentSection][key] = value;
                }
            }
        }

        file.close();
        return true;
    }

    std::string getValue(const std::string& section, const std::string& key) const {
        std::map<std::string, std::map<std::string, std::string> >::const_iterator sectionIt = config.find(section);
        if (sectionIt != config.end()) {
            std::map<std::string, std::string>::const_iterator keyIt = sectionIt->second.find(key);
            if (keyIt != sectionIt->second.end()) {
                return keyIt->second;
            }
        }
        return "";
    }

    void printConfig() const {
        std::map<std::string, std::map<std::string, std::string> >::const_iterator sectionIt;
        for (sectionIt = config.begin(); sectionIt != config.end(); ++sectionIt) {
            std::cout << "[" << sectionIt->first << "]" << std::endl;
            std::map<std::string, std::string>::const_iterator keyIt;
            for (keyIt = sectionIt->second.begin(); keyIt != sectionIt->second.end(); ++keyIt) {
                std::cout << keyIt->first << " = " << keyIt->second << std::endl;
            }
            std::cout << std::endl;
        }
    }
};

int main() {
    ConfigParser parser;
    if (parser.parse("config.ini")) {
        std::cout << "Config file parsed successfully." << std::endl;
        parser.printConfig();

        std::cout << "Value of 'key1' in 'section1': " << parser.getValue("section1", "key1") << std::endl;
    } else {
        std::cout << "Failed to parse config file." << std::endl;
    }
    return 0;
}

The same prompts can be applied to explaining code, generating tests, and even refactoring code: /explain focus on C++03 features, /tests for C++03 features, /refactor into C++03.

The following C++ standards have been tested working with GitLab Duo Code Suggestions:

  • C++03
  • C++11
  • C++14
  • C++17
  • C++20
  • C++23

You can find the source code in the Challenge - Refactor Cpp Standards Config Parser project.

There are many different combinations to refactor source code between the C++ standards. The following examples reflect on modernizing the C++ code with version upgrades, but also test a downgrade where ambitious developers wanted to use C++20 but had to use C++11 to support enterprise customer environments.

Migration: Refactor C++03 into C++14

The goal is to modernize the code, and understand the changes between C++03 and C++14. Open the 03.cpp source code in the IDE, select it, and use the following Chat prompt:

/refactor into C++14 and explain the changes

Potential Chat response:

…

Explanations of the changes:

1. Default constructor: Changed `ConfigParser() {}` to `ConfigParser() = default;` to use the default constructor.
2. File opening: Removed `.c_str()` when opening the file, as C++11 and later support `std::string` directly.
3. Lambda functions: Introduced a lambda function trim to handle string trimming, making the code more readable and reusable.
4. Auto keyword: Used `auto` for type deduction in various places, improving code readability.
5. Range-based for loops: In the `printConfig` method, replaced traditional for loops with range-based for loops, which are more concise and less error-prone.
6. Structured bindings: Used structured bindings in the range-based for loops to directly access key-value pairs.
7. If statements with initializers: In the `getValue` method, used if statements with initializers to combine the find operation and the check in a single line.
8. String member functions: Used `front()` and `back()` instead of array-style access for the first and last characters of strings.
9. Uniform initialization: Used uniform initialization `{}` instead of parentheses `()` for object creation.
10. Removed `std::endl`: Replaced `std::endl` with `'\n'` for better performance, as `std::endl` forces a flush of the output buffer.

These changes make the code more modern, concise, and potentially more efficient while maintaining the same functionality.

Changes reflected in code

Changes reflected in code 2

Downgrade: Refactor C++23 to C++11

Sometimes, developers started a new feature proof-of-concept in a modern C++ standard like C++20. The actual supported versions for enterprise customer environments require older compilers with C++11 support. Downgrading the versions come at a cost, and require evaluating which language features are still supported, and which other disadvantages could come across.

Open the 23.cpp source code in the IDE, select it, and use the following Chat prompt:

/refactor into C++11, explain the necessary changes and potential problems

Potential Duo Chat response:

The main changes required to make this code compatible with C++11 are:

1. Remove the C++23 standard comment, as it's no longer applicable.
2. Replace `std::string::find_first_not_of` and `std::string::find_last_not_of` with C++11 compatible alternatives in the trim function.
3. Use `const auto&` instead of `auto` for loop variables to avoid unnecessary copying.

Potential problems in the original code:

1. The original code used C++23 features, which might not be widely supported yet.
2. The `trim` function used `std::string::find_first_not_of` and `std::string::find_last_not_of`, which are not available in C++11.
3. The use of `auto` instead of `const auto&` in some loops could lead to unnecessary copying of complex objects.

These issues have been addressed in the refactored version. The `trim` function now uses `std::find_if_not` with lambda functions to achieve the same result in a C++11 compatible way. The use of `const auto&` ensures that no unnecessary copying occurs when iterating over the config map.

The  function now uses  with lambda functions to achieve the same result in a C++11 compatible way

The use of  ensures that no unnecessary copying occurs when iterating over the config map

Async practice: Test more version refactoring scenarios.

Explain and refactor COBOL

You can use GitLab Duo to explain the source code, analyze, fix and refactor for COBOL programs. I have never written nor learned COBOL, and found this helpful COBOL Programming Course with many examples.

I then asked Chat how to get started with COBOL, create a COBOL program, and compile a COBOL program on macOS.

Please explain what COBOL is and its syntax

Please create a COBOL program that shows the first steps

Tell me more about the COBOL compiler. Which system do I need? Can I do it on my macOS?

Asking GitLab Duo Chat to explain and its syntax

Open a COBOL program, select the source code, switch to Duo Chat and send the /explain prompt to explain purpose and functionality.

You can also refine the prompts to get more high-level summaries, for example:

/explain like I am five

Tip: Programming languages share similar algorithms and functionality. For COBOL, Chat offered to explain it using Python, and, therefore, I adjusted future prompts to ask for an explanation in Python.

/explain in a different programming language

You can also use the /refactor slash command prompt in Chat to improve the code quality, fix potential problems, and try to refactor COBOL into Python.

/refactor fix the environment error

/refactor fix potential problems

/refactor into Python

The GitLab Duo Coffee Chat - Challenge: Explain and Refactor COBOL programs recording shows all discussed steps in a practical use case, including how to find a missing period:

Refactor a language into another language

Modernization and code quality improvements sometimes require the change of a programming language. Similar refactor prompts with GitLab Duo can help speed up the migration process. The COBOL example with Python is just one of many requirements in enterprise environments -- let's dive into more use cases.

Refactor C to Rust

In early 2024, several programming languages, like C, have been called out for not being memory safe. The recommendations for future projects include memory safe languages like Rust. But how do you start a migration, and what are the challenges?

Let's try it with a simple example in C. The code was generated using Code Suggestions and should print the basic operating system information, like the name, version, and platform. The C code compiles cross-platform on Windows, Linux, and macOS.

// Read OS files to identify the platform, name, versions
// Print them on the terminal
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#ifdef _WIN32
    #include <windows.h>
#elif __APPLE__
    #include <sys/utsname.h>
#else
    #include <sys/utsname.h>
#endif

void get_os_info() {
    #ifdef _WIN32
        OSVERSIONINFOEX info;
        ZeroMemory(&info, sizeof(OSVERSIONINFOEX));
        info.dwOSVersionInfoSize = sizeof(OSVERSIONINFOEX);
        GetVersionEx((OSVERSIONINFO*)&info);

        printf("Platform: Windows\n");
        printf("Version: %d.%d\n", info.dwMajorVersion, info.dwMinorVersion);
        printf("Build: %d\n", info.dwBuildNumber);
    #elif __APPLE__
        struct utsname sys_info;
        uname(&sys_info);

        printf("Platform: macOS\n");
        printf("Name: %s\n", sys_info.sysname);
        printf("Version: %s\n", sys_info.release);
    #else
        struct utsname sys_info;
        uname(&sys_info);

        printf("Platform: %s\n", sys_info.sysname);
        printf("Name: %s\n", sys_info.nodename);
        printf("Version: %s\n", sys_info.release);
    #endif
}

int main() {
    get_os_info();
    return 0;
}

Open the source code in os.c in JetBrains CLion, for example. Select the source code and use the Chat prompt /explain to explain purpose and functionality. Next, use /refactor in the Chat prompt to refactor the C code, and then take it one step further: /refactor into Rust.

Initialize a new Rust project (Tip: Ask Duo Chat), and copy the generated source code into the src/main.rs file. Run cargo build to compile the code.

Initialize a new Rust project, and copy the generated source code into the  file. Run  to compile the code.

In the GitLab Duo Coffee Chat: Challenge - Refactor C into Rust recording, you can learn all steps, and additionally, you'll see a compilation error which gets fixed with the help of Chat and /refactor slash command. The session also shows how to improve the maintanability of the new Rust code by adding more error handling.

Refactor Perl to Python

That one script that runs on production servers, does its job, the author left the company ten years ago, and nobody wants to touch it. The problem might also apply to multiple scripts, or even a whole application. A decision was made to migrate everything to modern Python 3, with the goal to modernize the code, and understand the changes between Perl and Python.

A customer recently asked in a GitLab Duo workshop whether a direct migration is possible using GitLab Duo. Short answer: Yes, it is. Longer answer: You can use refined Chat prompts to refactor Perl code into Python, similar to other examples in this article.

Open the script.pl source code in IDE, select it, and open Chat.

#!/usr/bin/perl
use strict;
use warnings;

open my $md_fh, '<', 'file.md' or die "Could not open file.md: $!";

my $l = 0;
my $e = 0;
my $h = 0;

while (my $line = <$md_fh>) {
  $l++;
  if ($line =~ /^\s*$/) {
    $e++;
    next;
  }
  if ($line =~ /^#+\s*(.+)/) {
    print "$1\n";
    $h++; 
  }
}

print "\nS:\n"; 
print "L: $l\n";
print "E: $e\n"; 
print "H: $h\n";

You can use the following prompts to:

  1. /explain its purpose, and /refactor to improve the code.
  2. /refactor into Python to get a working Python script.

Refactor into Python

Tip: You can refactor Perl code into more target languages. The GitLab Duo Coffee Chat: Challenge - Refactor Perl to Python recording shows PHP, Ruby, Rust, Go, Java, VB.NET, C#, and more.

If you want to continue using Perl scripts, you can configure Perl as additional language in Duo Code Suggestions. Chat already understands Perl and can help with questions and slash command prompts, as you can see in the following recording.

More Refactoring Hands-on

Refactor JavaScript

Eddie Jaoude shows how to refactor JavaScript to improve code quality or add functionality in a practical example.

Refactor Bash to ZSH or SH

I have used Bash as a shell for 20 years and most recently switched to ZSH on macOS. This resulted in script not working, or unknown errors in my terminal. Another use case for refactoring are shell limitations – some operating systems or Linux/Unix distributions do not provide Bash, only SH, for example, Alpine.

Refactor shell scripts

The GitLab Duo Coffee Chat: Challenge - Refactor Shell Scripts shows an example with a C program that can tail syslog files, and a build script written in Bash. Throughout the challenge, Chat is queried with /explain and /refactor prompts to improve the code. It is also possible to refactor Bash into POSIX-compliant SH or ZSH. The session concludes with asking Chat to provide five different Shell script implementations, and explain the key summaries.

More use cases and tutorials

Key takeaways

  1. GitLab Duo provides efficient help with explaining and refactoring code.
  2. You can refactor code between language standards, and ask follow-up questions in Chat.
  3. Code Suggestions prompts can generate specific language standards, and code completion respects the current code context.
  4. Refactoring code into new programming languages helps with longer term migration and modernization plans.
  5. Code can be "downgraded" into older system's supported language standards.
  6. GitLab Duo can explain complex code and programming languages with different programming language examples.
  7. The update to Anthropic Claude 3.5 on GitLab.com has improved the quality and speed of Code Suggestions and Chat once again (self-managed upgrade to 17.3 recommended).
  8. There are no boundaries except your imagination, and production pain points.

Learn more about efficient Code Suggestions and Chat workflows, and start your AI-powered code refactoring journey with GitLab Duo today!

Start your 60-day free trial of GitLab Duo!

We want to hear from you

Enjoyed reading this blog post or have questions or feedback? Share your thoughts by creating a new topic in the GitLab community forum. Share your feedback

Ready to get started?

See what your team could do with a unified DevSecOps Platform.

Get free trial

Find out which plan works best for your team

Learn about pricing

Learn about what GitLab can do for your team

Talk to an expert