PowerShellTestVisitor.java
package org.egothor.methodatlas.discovery.powershell.internal;
import java.util.ArrayList;
import java.util.List;
import org.egothor.methodatlas.discovery.powershell.parser.PowerShellTestBaseVisitor;
import org.egothor.methodatlas.discovery.powershell.parser.PowerShellTestParser;
/**
* ANTLR4 visitor that walks a PowerShell/Pester parse tree and collects
* information about {@code It} test blocks.
*
* <p>Instances are single-use: create one per source file, call
* {@link #visit(org.antlr.v4.runtime.tree.ParseTree)}, then read results via
* {@link #getDiscoveredCommands()}.</p>
*
* <h2>Detection</h2>
* <p>Every {@code It} block encountered during traversal is recorded,
* regardless of nesting depth inside {@code Describe} or {@code Context}
* blocks. The grammar is case-insensitive so {@code it}, {@code IT}, and
* {@code It} all match.</p>
*
* <h2>Tag extraction</h2>
* <p>Tags are collected from the {@code -Tag} parameter of the {@code It}
* line. Both the array form ({@code -Tag @("a","b")}) and the plain
* comma-separated form ({@code -Tag "a","b"}) are handled by the grammar
* and exposed via {@link PowerShellTestParser.ParamValueContext#string_()}.</p>
*/
public final class PowerShellTestVisitor extends PowerShellTestBaseVisitor<Void> {
private static final int MIN_QUOTED_LENGTH = 2;
private final List<CommandInfo> discoveredCommands = new ArrayList<>();
/**
* Visits an {@code It} block, extracts the test name and any tags, and
* records the result. Recursion via {@link #visitChildren(org.antlr.v4.runtime.tree.RuleNode)}
* ensures that nested {@code It} blocks (rare in practice) are also captured.
*
* @param ctx parse-tree context for the {@code It} block
* @return {@code null} (visitor protocol)
*/
@Override
public Void visitItBlock(PowerShellTestParser.ItBlockContext ctx) {
String name = unquote(ctx.string_());
List<String> tags = extractTags(ctx.itArg());
int beginLine = ctx.start.getLine();
int endLine = ctx.stop != null ? ctx.stop.getLine() : beginLine;
discoveredCommands.add(new CommandInfo(name, List.copyOf(tags), beginLine, endLine));
// Recurse into nested It blocks inside the script block
return visitChildren(ctx);
}
/**
* All {@code It} blocks found in the file after visiting.
*
* @return unmodifiable list of discovered commands
*/
public List<CommandInfo> getDiscoveredCommands() {
return List.copyOf(discoveredCommands);
}
// ── Private helpers ───────────────────────────────────────────────
/**
* Removes surrounding quotes from a string literal and un-escapes
* PowerShell escape sequences.
*
* <ul>
* <li>Double-quoted: surrounding {@code "} removed; {@code `"} → {@code "}</li>
* <li>Single-quoted: surrounding {@code '} removed; {@code ''} → {@code '}</li>
* </ul>
*
* @param ctx string context from the grammar; may be {@code null}
* @return unquoted string; empty when {@code ctx} is {@code null}
*/
private static String unquote(PowerShellTestParser.String_Context ctx) {
if (ctx == null) {
return "";
}
String raw = ctx.getText();
if (raw.length() < MIN_QUOTED_LENGTH) {
return raw;
}
if (raw.startsWith("\"") && raw.endsWith("\"")) {
return raw.substring(1, raw.length() - 1).replace("`\"", "\"");
}
if (raw.startsWith("'") && raw.endsWith("'")) {
return raw.substring(1, raw.length() - 1).replace("''", "'");
}
return raw;
}
/**
* Extracts tag values from the {@code itArg} list of an {@code It} block.
*
* <p>Looks for an {@code itArg} whose MINUS token and IDENTIFIER token are
* both present and whose identifier text equals {@code "tag"}
* (case-insensitive match handled by the grammar's {@code caseInsensitive}
* option). The tags are then read from the {@code paramValue} sub-rule
* that consumes all immediately-following quoted strings.</p>
*
* @param args itArg context list from the {@code itBlock} rule
* @return mutable list of tag strings (possibly empty)
*/
private static List<String> extractTags(List<PowerShellTestParser.ItArgContext> args) {
List<String> tags = new ArrayList<>();
for (PowerShellTestParser.ItArgContext arg : args) {
if (arg.MINUS() != null
&& arg.IDENTIFIER() != null
&& "tag".equalsIgnoreCase(arg.IDENTIFIER().getText())
&& arg.paramValue() != null) {
for (PowerShellTestParser.String_Context sc : arg.paramValue().string_()) {
tags.add(unquote(sc));
}
}
}
return tags;
}
}