Skip to main content
The 2024 Developer Survey results are live! See the results
1586
1

This article is written using Ballerina Swan Lake Update 7.

In this article, we are learning what code analyzers in Ballerina Swan Lake are, the possible use cases for code analyzers, and how we can create a simple code analyzer that restricts the module imports of a Ballerina project to allow only Ballerina Platform-provided modules.

Using only Ballerina Platform-provided dependencies can be beneficial when you are concerned about the possible security vulnerabilities that can be introduced with third-party dependencies. Furthermore, Ballerina Platform dependencies are guaranteed to receive long-term support. However, this is just a sample use case meant to walk you through the steps of creating a code analyzer.

What is a compiler plugin?

A compiler plugin is a software component that extends the functionality of the compiler. It allows developers to customize and augment the behavior of the compiler without modifying the source code directly.

What is a code analyzer?

Code analyzers are a type of compiler plugins that work in conjunction with compilers or build systems to automatically inspect and analyze source code for various coding issues, including potential bugs, security vulnerabilities, code style violations, and adherence to coding standards. It helps to identify the issues in the development process early and can significantly improve code quality and maintainability.

To get a better idea about code analyzers, let’s look at some use cases of them.

  1. Bug and error detection: Code analyzers can identify potential bugs and errors in the source code. They can detect issues such as null pointer dereferences, array index out-of-bounds errors, resource leaks, and other common programming mistakes.
  2. Code duplication detection: Code analysis tools can identify duplicated code segments, promote code reuse, reduce maintenance efforts, and avoid inconsistencies.
  3. Coding standards and best practices: Code analyzers can enforce coding standards and best practices within the development team. They can check for adherence to style guidelines, naming conventions, and other coding rules, promoting consistency across the codebase.
  4. Performance optimization: Code analyzers can identify performance bottlenecks and suggest potential optimizations. They can highlight areas where code improvements may lead to faster execution or reduced memory usage.

Code Analyzers in Ballerina

Ballerina provides a set of APIs that developers can use to develop compiler plugins.

Figure 1: Simplified class diagram for compiler plugin APIs Figure 1: Simplified class diagram for compiler plugin APIs

Within a compiler plugin, we can add multiple code analyzers. They will be executed in the order of registering them within the compiler plugin. Within a single code analyzer, we can define multiple tasks. Two types of tasks can be attached to the code analysis context.

  1. Syntax node analysis task

A syntax node analysis task performs a user-defined operation on one or more types of nodes of the AST (Abstract Syntax Tree). The task will be executed for every node of the specified kind(s); hence, manual AST traversal is not required.

  1. Compilation analysis task

A compilation analysis task provides access to the compiled package. The source files, semantic models, package resolutions, etc., can be analyzed with the Ballerina-provided APIs.

The attached tasks can be a combination of syntax node analysis tasks and compiler analysis tasks. When both types of tasks are present, compiler analysis tasks will be executed after executing all syntax node analysis tasks. Tasks of a given type will be executed in the order of registering them in the init() method of the analyzer. All analysis tasks are executed as a post-compilation step.


Time to Get Our Hands Dirty!

Let's walk through the steps of creating a code analyzer. Here, we are creating a module import restrictor, that restricts the use of imports only to Ballerina Platform-provided imports. The code analyzer will provide an error diagnostic if the organization name of an import does not start with the prefix ballerina.

Step 1: Implement the Code Analyzer Logic

The code analyzer logic is implemented in Java with the help of the APIs provided by Ballerina.

/**
 * Compiler plugin for import restriction. This allows only Ballerina platform modules to be imported.
 */
public class ImportRestrictionPlugin extends CompilerPlugin {
    @Override
    public void init(CompilerPluginContext compilerPluginContext) {
        compilerPluginContext.addCodeAnalyzer(new ImportRestrictionAnalyzer());
    }
}

Within the ImportRestrictionAnalyzer, I have added a syntax node analysis task because it suits our purpose of restricting third-party imports. This task is executed for all nodes of the kind IMPORT_DECLARATION in the AST, meaning it will be invoked for all module imports defined in the Ballerina code.

/**
 * Code analyzer for import restriction.
 */
public class ImportRestrictionAnalyzer extends CodeAnalyzer {

    @Override
    public void init(CodeAnalysisContext codeAnalysisContext) {
        codeAnalysisContext.addSyntaxNodeAnalysisTask(
                new ImportRestrictionAnalysisTask(), SyntaxKind.IMPORT_DECLARATION);
    }
}

Let’s look at a simple Ballerina program and a visual representation of the AST.

import ballerina/io;
import ballerina/os;

public function main() {
    // Returns the environment variable value associated with the `HTTP_PORT`.
    string port = os:getEnv("HTTP_PORT");
    io:println("HTTP_PORT: ", port);
}

Figure 2: Simplified AST for the sample program with imports Figure 2: Simplified AST for the sample program with imports

The task will only run for nodes 2 and 3, which are of IMPORT_DECLARATION kind.

Now, for each of these import declarations, we have to check if the organization name of the import starts with the ballerina prefix.

public class ImportRestrictionAnalysisTask implements AnalysisTask<SyntaxNodeAnalysisContext> {
    private static final String THIS_PKG_ORG = "gayaldassanayake";
    private static final String THIS_PKG_NAME = "import_restrictor";

    @Override
    public void perform(SyntaxNodeAnalysisContext context) {
        Optional<ImportOrgNameNode> importOrgNameOpt = ((ImportDeclarationNode)context.node()).orgName();
        String importModuleName = ((ImportDeclarationNode)context.node()).moduleName().get(0).text();

        String packageOrgName = context.currentPackage().packageOrg().value();
        String packageName = context.currentPackage().packageName().value();

        String importOrgName = importOrgNameOpt.map(node -> node.orgName().text()).orElse(packageOrgName);

        // Do not warn if package org starts with "ballerina"
        boolean isBallerinaImport = importOrgName.startsWith("ballerina");

        // ....
    }
}

However, we have to exempt users from the rule of importing only Ballerina Platform modules in two cases.

  1. Importing the code analyzer — We have to allow the developer to import this code analyzer so that they can enable import restrictions on their project.

  2. Importing modules of the developer’s project — To have a useful separation of logic, the developer needs to have modules and import them within the same project.

        // Do not warn if the import is the import restrictor compiler plugin
        boolean isCompPluginImport = importOrgName.equals(THIS_PKG_ORG) && importModuleName.equals(THIS_PKG_NAME);
        // Do not warn if the import is a module of this package
        boolean isSubModule = importOrgName.equals(packageOrgName) && importModuleName.equals(packageName);

We will proceed to issue an error if all isBallerinaImport, isCompilerPluginImport, and isSubModule variables are false.

Ballerina has provided a set of APIs to properly display any diagnostics to the developer. The diagnostic severity can be INTERNAL, HINT, INFO, WARNING, or ERROR. I have added an error diagnostic as shown below.

        if (!(isBallerinaImport || isCompPluginImport || isSubModule)) {
            DiagnosticInfo diagnosticInfo = new DiagnosticInfo(
                    RestrictorDiagnosticCodes.IMPORT_VIOLATION.getCode(),
                    RestrictorDiagnosticCodes.IMPORT_VIOLATION.getMessage(),
                    RestrictorDiagnosticCodes.IMPORT_VIOLATION.getSeverity());
            context.reportDiagnostic(DiagnosticFactory.createDiagnostic(diagnosticInfo, context.node().location()));
        }
public enum RestrictorDiagnosticCodes {
    IMPORT_VIOLATION(
            "IMPORT_VIOLATION",
            "Only Ballerina platform modules are allowed to be imported",
            DiagnosticSeverity.ERROR);


    private final String code;
    private final String message;
    private final DiagnosticSeverity severity;

    RestrictorDiagnosticCodes(String code, String message, DiagnosticSeverity severity) {
        this.code = code;
        this.message = message;
        this.severity = severity;
    }

    public String getCode() {
        return code;
    }

    public String getMessage() {
        return message;
    }

    public DiagnosticSeverity getSeverity() {
        return severity;
    }
}

Step 2: Create the Code Analyzer Package

Once we have completed the Java implementation, we should generate a jar executable. Next, we have to create a new Ballerina project.

% bal new import-restrictor
Created new package 'import_restrictor' at /Users/gayaldassanayake/Downloads/import-restrictor.

This Ballerina project acts as a wrapper for the above jar. We need to create a CompilerPlugin.toml file in the root directory of the project.

import-restrictor
   ├── .devcontainer.json
   ├── Ballerina.toml
   ├── CompilerPlugin.toml
   ├── Config.toml
   ├── main.bal
   └── Package.md

It should contain the class name and the path for the compiler plugin jar and additional dependency jars in that TOML file.

[plugin]
class = "io.gayal.restrictor.ImportRestrictionPlugin"

[[dependency]]
path = "/Users/gayaldassanayake/Documents/devrel/import-restriction-analyzer/ImportRestrictor/build/libs/ImportRestrictor-1.0.0.jar"

# Add additional jar dependencies needed for the java implementation
# [[dependency]]
# path = /path/to/dependency

Now we can pack and push the compiler plugin to the Ballerina Central so that users can use it to get the code complexity data for their projects.

% bal pack                  
Compiling source
       gayaldassanayake/import_restrictor:0.1.0

Creating bala
       target/bala/gayaldassanayake-import_restrictor-any-0.1.0.bala

% bal push
gayaldassanayake/import_restrictor:0.1.0 [import-restrictor/target/bala/gayaldassanayake-import_restrictor -> dev-cengayaldassanayake/import_restrictor:0.1.0] 100%
gayaldassanayake/import_restrictor:0.1.0 pushed to central successfully

Step 3: Using the Compiler Plugin

For the compiler plugin to be used, a user can import the Compiler plugin Ballerina package as an unnamed import:

import gayaldassanayake/import_restrictor as _;.

import gayaldassanayake/import_restrictor as _;

import ballerina/log;
import ballerinax/nats;

import gayaldassanayake/inventory_manager;

public type Order record {
    int orderId;
    string productName;
    decimal price;
    boolean isValid;
};

// Binds the consumer to listen to the messages published to the 'orders.valid' subject.
service "orders.valid" on new nats:Listener(nats:DEFAULT_URL) {

    remote isolated function onMessage(Order 'order) returns error? {
        if 'order.isValid {
            log:printInfo(string `Received valid order for ${'order.productName}`);
            inventory_manager:acceptOrder('order);
        }
    }
}

In line 1, I have imported the Compiler Plugin import-restrictor, which ensures that there cannot be any third-party imports in the current module. Since imports in lines 3 and 4 are Ballerina Platform-provided, they won���t produce an error. However, the import in line 6 which is not platform-provided, is not allowed and an error will be shown.

If you have the Ballerina vscode plugin installed, the error will be shown when you hover over to inventory_manager import.

Figure 3: vscode error diagnostic Figure 3: vscode error diagnostic

If you run bal build, the output will contain the same error.

% bal build
Compiling source
        gayaldassanayake/restrictor_user:0.1.0
ERROR [main.bal:(6:1,6:43)] Only Ballerina platform modules are allowed to be imported
error: compilation contains errors

You can find the complete implementation of the import restriction analyzer here.


If you have more questions about compiler plugins, feel free to raise them on the Ballerina Discord server or in Stackoverflow with the ballerina tag.

Happy coding in Ballerina Swan Lake!