GoTestVisitor.java

package org.egothor.methodatlas.discovery.go.internal;

import java.util.ArrayList;
import java.util.List;

import org.egothor.methodatlas.discovery.go.parser.GoTestBaseVisitor;
import org.egothor.methodatlas.discovery.go.parser.GoTestParser;

/**
 * ANTLR4 visitor that walks a Go parse tree produced by the {@code GoTest}
 * grammar and collects structural information about test functions.
 *
 * <p>Instances are single-use: create one per source file, call
 * {@link #visit(org.antlr.v4.runtime.tree.ParseTree)}, then read results via
 * {@link #getDiscoveredMethods()} and {@link #getPackageName()}.</p>
 *
 * <h2>Test detection</h2>
 * <p>A top-level function is identified as a test if:</p>
 * <ol>
 *   <li>Its name starts with {@code "Test"}.</li>
 *   <li>The character immediately after {@code "Test"} (if any) is upper-case
 *       or an underscore — per {@code go help testfunc}.</li>
 *   <li>Its parameter list contains a parameter whose type is
 *       {@code *testing.T}.</li>
 * </ol>
 * <p>Methods (functions with a receiver) and non-test top-level functions are
 * silently ignored.</p>
 */
public final class GoTestVisitor extends GoTestBaseVisitor<Void> {

    private static final int TEST_PREFIX_LENGTH = "Test".length();

    private String packageName = "unknown";
    private final List<MethodInfo> discoveredMethods = new ArrayList<>();

    /** {@inheritDoc} */
    @Override
    public Void visitPackageDecl(GoTestParser.PackageDeclContext ctx) {
        packageName = ctx.IDENTIFIER().getText();
        return null;
    }

    /**
     * Visits a top-level function declaration and records it when it satisfies
     * the Go test-function convention.
     *
     * @param ctx parse-tree context for the function declaration
     * @return {@code null} (visitor protocol)
     */
    @Override
    public Void visitFuncDecl(GoTestParser.FuncDeclContext ctx) {
        String name = ctx.IDENTIFIER().getText();
        if (!isTestFunctionName(name)) {
            return null;
        }
        if (!hasTestingTParameter(ctx.parameters())) {
            return null;
        }
        int beginLine = ctx.start.getLine();
        int endLine   = ctx.stop != null ? ctx.stop.getLine() : beginLine;
        discoveredMethods.add(new MethodInfo(name, beginLine, endLine));
        return null;
    }

    // visitMethodDecl intentionally not overridden: Go test functions are
    // always top-level funcs, never methods on a type.

    // ── Result accessors ──────────────────────────────────────────────

    /**
     * Package name extracted from the {@code package} declaration, or
     * {@code "unknown"} when no declaration was found.
     *
     * @return package name; never {@code null}
     */
    public String getPackageName() {
        return packageName;
    }

    /**
     * All test functions found in the file after visiting.
     *
     * @return unmodifiable list of discovered test methods
     */
    public List<MethodInfo> getDiscoveredMethods() {
        return List.copyOf(discoveredMethods);
    }

    // ── Private helpers ───────────────────────────────────────────────

    /**
     * Returns {@code true} when {@code name} follows the Go test-function
     * naming convention: starts with {@code "Test"}, and the next character
     * (if present) is upper-case or {@code '_'}.
     *
     * @param name function name to check
     * @return {@code true} for valid test-function names
     */
    private static boolean isTestFunctionName(String name) {
        if (!name.startsWith("Test")) {
            return false;
        }
        if (name.length() == TEST_PREFIX_LENGTH) {
            return true; // bare "Test" is valid per go test spec
        }
        char next = name.charAt(TEST_PREFIX_LENGTH);
        return Character.isUpperCase(next) || next == '_';
    }

    /**
     * Returns {@code true} when the parameter list contains a parameter of
     * type {@code *testing.T}.
     *
     * @param ctx parameter-list context from a function declaration
     * @return {@code true} if {@code *testing.T} is present
     */
    private static boolean hasTestingTParameter(GoTestParser.ParametersContext ctx) {
        if (ctx == null) {
            return false;
        }
        GoTestParser.ParamListContext pl = ctx.paramList();
        if (pl == null) {
            return false;
        }
        for (GoTestParser.ParamDeclContext pd : pl.paramDecl()) {
            if (isPointerToTestingT(pd.type_())) {
                return true;
            }
        }
        return false;
    }

    /**
     * Returns {@code true} when {@code t} represents the type {@code *testing.T}.
     *
     * <p>The check inspects the ANTLR4 parse tree structurally:</p>
     * <ol>
     *   <li>The type_ must have a STAR token (pointer operator).</li>
     *   <li>The inner type_ must be a {@code typeName} whose text equals
     *       {@code "testing.T"}.</li>
     * </ol>
     *
     * @param t type context from a parameter declaration
     * @return {@code true} for {@code *testing.T}
     */
    private static boolean isPointerToTestingT(GoTestParser.Type_Context t) {
        if (t == null) {
            return false;
        }
        // *testing.T → STAR type_ where the inner type_ is typeName "testing.T"
        if (t.STAR() == null) {
            return false;
        }
        List<GoTestParser.Type_Context> innerTypes = t.type_();
        if (innerTypes.isEmpty()) {
            return false;
        }
        GoTestParser.TypeNameContext tn = innerTypes.get(0).typeName();
        return tn != null && "testing.T".equals(tn.getText());
    }
}