Static Code Analysis to the Rescue

Gayan Perera
4 min readJun 26, 2024

--

Photo by Etactics Inc on Unsplash

Recently I was working with my fellow Ops Engineer to build a more developer-friendly and developer-own CI Pipeline for our new breed of modules. While we were building this we wanted to enable Testcontainers for developers in CI Pipeline so that developers can do better Automated Tests with much control over the external dependencies.

Now one struggle for us was to provide a docker daemon in the environment our builds are executing since Testcontainers need a docker API to work its magic. After some experimentation, we land on Kubedock

Which was a better solution since our builds were running on a Kubernetes cluster. Now one hard requirement we found was that we had to use a network alias to avoid conflicting containers where two tests end up using the same container.

So we have to find a way to make sure developers always use a network alias when they configure their Testcontainer instances. A very naive approach is to use a shared library function to create a new instance of a Testcontainer. But that will limit how much Testcontainer functionality the developers can use since they will not be able to access Testcontainer API. Even if we just expose a configured Testcontainer instance, then we may need to do that for all pre-configured Testcontainer types that Testcontainers provide.

A better approach is to let developers be developers and use the Testcontainer library the way they want to use it and add a compile-time check to make sure the Testcontainer configuration is using the network alias, If it is missing the code will not be compiled.

Now different languages have different approaches to this. Following is how you can do this with Java with the help of google library

Following a simple implementation of such a linting rule and its test code

package org.gap.java.build.rules;

import com.google.errorprone.util.ASTHelpers;
import com.sun.source.tree.MethodInvocationTree;
import com.sun.source.util.TreeScanner;
import com.sun.tools.javac.code.Symbol.MethodSymbol;

public class MethodTreeVisitor extends TreeScanner<Void, Void> {
private String methodName;
private boolean found = false;
private boolean visited = false;

public MethodTreeVisitor(String methodName) {
this.methodName = methodName;
}

@Override
public Void visitMethodInvocation(MethodInvocationTree node, Void p) {
this.visited = true;
if (found) {
return null;
}
MethodSymbol symbol = ASTHelpers.getSymbol(node);

if (symbol.getSimpleName().contentEquals(methodName)) {
found = true;
}
return super.visitMethodInvocation(node, p);
}

public boolean isFound() {
return found;
}

public boolean isVisited() {
return visited;
}
}
package org.gap.java.build.rules;

import java.util.List;

import com.google.auto.service.AutoService;
import com.google.errorprone.BugPattern;
import com.google.errorprone.BugPattern.SeverityLevel;
import com.google.errorprone.VisitorState;
import com.google.errorprone.bugpatterns.BugChecker;
import com.google.errorprone.bugpatterns.BugChecker.MethodTreeMatcher;
import com.google.errorprone.matchers.Description;
import com.google.errorprone.util.ASTHelpers;
import com.sun.source.tree.ExpressionStatementTree;
import com.sun.source.tree.MethodInvocationTree;
import com.sun.source.tree.MethodTree;
import com.sun.source.tree.StatementTree;
import com.sun.source.tree.Tree.Kind;
import com.sun.source.tree.VariableTree;
import com.sun.tools.javac.code.Symbol.ClassSymbol;
import com.sun.tools.javac.code.Type;
import com.sun.tools.javac.code.Types;
import com.sun.tools.javac.model.JavacElements;

@BugPattern(name = "MustHaveNetworkAliases",
summary = "The withNetworkAliases method must be used with a random alias to avoid network collisions in CI.",
severity = SeverityLevel.ERROR)
@AutoService(BugChecker.class)
public class KubedocNetworkAliasCheck extends BugChecker implements MethodTreeMatcher {

@Override
public Description matchMethod(MethodTree tree, VisitorState state) {
Types types = Types.instance(state.context);
JavacElements elements = JavacElements.instance(state.context);
ClassSymbol typeElement = elements.getTypeElement("org.testcontainers.containers.GenericContainer");
Type expectedBaseType = (Type) typeElement.asType();

List<? extends StatementTree> statements = tree.getBody().getStatements();
MethodTreeVisitor visitor = new MethodTreeVisitor("withNetworkAliases");
for (StatementTree statement : statements) {
switch (statement.getKind()) {
case VARIABLE: {
VariableTree variableTree = (VariableTree) statement;
Type varType = ASTHelpers.getType(variableTree.getType());
if (types.isAssignable(types.erasure(varType), expectedBaseType)) {
variableTree.getInitializer().accept(visitor, null);
}
break;
}
case EXPRESSION_STATEMENT: {
ExpressionStatementTree expressionStatement = (ExpressionStatementTree) statement;
Type resultType = ASTHelpers.getResultType(expressionStatement.getExpression());
if (types.isAssignable(types.erasure(resultType), expectedBaseType)) {
expressionStatement.accept(visitor, null);
}
break;
}
default: {
// do nothing
}
}
if (visitor.isFound()) {
break; // break out of the loop since we already found
// what we are looking for.
}
}
if (visitor.isVisited() && !visitor.isFound()) {
return buildDescription(tree).setMessage(
String.format("The method %s must include a network alias on the testcontainers.", tree.getName())).build();
}
return Description.NO_MATCH;
}
}
package org.gap.java.build.rules;

import org.junit.jupiter.api.Test;

import com.google.errorprone.CompilationTestHelper;

class KubedocNetworkAliasCheckTest {
CompilationTestHelper helper = CompilationTestHelper.newInstance(KubedocNetworkAliasCheck.class, getClass());

@Test
void whenCreatedWithGenericContainerBuilderWithNetworkAliases() {
helper.addSourceLines("GenericContainerCreation.java", """
import org.testcontainers.containers.GenericContainer;
public class GenericContainerCreation {
public void setup() {
GenericContainer<?> genericContainer = new GenericContainer<>("alpine").withCommand("echo");
genericContainer.withNetworkAliases("k3s").withLabel("app", "echo");
genericContainer.start();
}
}
""".split("\n")).doTest();
}

@Test
void whenCreatedWithGenericContainerBuilderWithNoNetworkAliases() {
helper.addSourceLines("GenericContainerCreation.java", """
import org.testcontainers.containers.GenericContainer;
public class GenericContainerCreation {
// BUG: Diagnostic contains: The method setup must include a network alias on the testcontainers.
public void setup() {
GenericContainer<?> genericContainer = new GenericContainer<>("alpine").withCommand("echo");
genericContainer.withLabel("app", "echo");
genericContainer.start();
}
}
""".split("\n")).doTest();
}

@Test
void whenCreatedWithGenericContainerBuilderWithNoNetworkAliases_FieldVariant() {
helper.addSourceLines("GenericContainerCreation.java", """
import org.testcontainers.containers.GenericContainer;
public class GenericContainerCreation {
private GenericContainer<?> genericContainer;
// BUG: Diagnostic contains: The method setup must include a network alias on the testcontainers.
public void setup() {
genericContainer = new GenericContainer<>("alpine").withCommand("echo");
genericContainer.start();
}
}
""".split("\n")).doTest();
}

@Test
void whenNoTestContainersUsed() {
helper.addSourceLines("GenericContainerCreation.java", """
import org.testcontainers.containers.GenericContainer;
public class GenericContainerCreation {
public void setup() {
// no testcontainers used
}
}
""".split("\n")).doTest();
}
}

The library provides a better way to write your rules using the Javac Model and also test your rules easily as you can see above. An example project can be found with all required configurations and the above rule code at https://github.com/gayanper/gap-techblog-src/tree/main/javac-static-analysis. This shows how to build your rules and how you can use them in a project, in this example, the usage can be found in the “app” maven module.

To get started you can look at existing rules in the library and also follow the guide https://github.com/google/error-prone/wiki/Writing-a-check. You can get help from AI Assistants like Github Copilot and ChatGPT as well to get some idea on how to interpret certain code segments using the Javac Model.

Now next time you find a problem like this, You can choose to solve it by using a linting/static analysis solution rather than restricting the developer from using external library API.

Happing Coding 👨‍💻.

--

--