Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
213 changes: 110 additions & 103 deletions src/main/java/com/seleniumboot/migrator/Analyzer.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration;
import com.github.javaparser.ast.body.ConstructorDeclaration;
import com.github.javaparser.ast.body.FieldDeclaration;
import com.github.javaparser.ast.body.MethodDeclaration;
import com.github.javaparser.ast.expr.MethodCallExpr;
import com.github.javaparser.ast.expr.NameExpr;
import com.github.javaparser.ast.expr.ObjectCreationExpr;
Expand All @@ -15,10 +14,11 @@
import com.github.javaparser.ast.expr.FieldAccessExpr;
import com.github.javaparser.ast.expr.ThisExpr;
import com.github.javaparser.ast.Node;
import com.github.javaparser.ast.stmt.ExpressionStmt;
import com.github.javaparser.ast.stmt.Statement;
import com.github.javaparser.ast.expr.VariableDeclarationExpr;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;

import javax.xml.parsers.DocumentBuilderFactory;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
Expand All @@ -30,7 +30,6 @@
import static com.seleniumboot.migrator.Finding.Status.AUTO;
import static com.seleniumboot.migrator.Finding.Status.MANUAL;

/** Read-only static analysis: parses Java sources and reports patterns that map onto Selenium Boot. */
public final class Analyzer {

private static final Set<String> DRIVER_LIFECYCLE_ANNOTATIONS = Set.of(
Expand All @@ -45,114 +44,114 @@
try (Stream<Path> s = Files.walk(root)) {
files = s.filter(p -> p.toString().endsWith(".java")).sorted().toList();
}

List<Finding> findings = new ArrayList<>();
LocatorStats locatorStats = new LocatorStats();
int parsed = 0;
List<String> unparsable = new ArrayList<>();

for (Path f : files) {
var result = parser.parse(f);

if (result.getResult().isEmpty() || !result.isSuccessful()) {
unparsable.add(root.relativize(f).toString());
continue;
}

parsed++;
scan(result.getResult().get(), root.relativize(f).toString(), findings, locatorStats);

Check failure on line 62 in src/main/java/com/seleniumboot/migrator/Analyzer.java

View workflow job for this annotation

GitHub Actions / Unit Tests

method scan in class com.seleniumboot.migrator.Analyzer cannot be applied to given types;

Check failure on line 62 in src/main/java/com/seleniumboot/migrator/Analyzer.java

View workflow job for this annotation

GitHub Actions / Unit Tests

method scan in class com.seleniumboot.migrator.Analyzer cannot be applied to given types;
}
List<String> detectedTechnologies = BuildFileAnalyzer.detect(root);
return new Report(files.size(), parsed, unparsable, findings, detectedTechnologies,
recognizedTechnologies(detectedTechnologies), locatorStats.counts());

String technologies = detectTechnologies(root);

return new Report(files.size(), parsed, unparsable, findings, technologies);
}

/** Analyze a single pasted source string. */
public Report analyzeSource(String source) {
var result = parser.parse(source);
List<Finding> findings = new ArrayList<>();
LocatorStats locatorStats = new LocatorStats();

if (result.getResult().isEmpty() || !result.isSuccessful()) {
return new Report(1, 0, List.of("<pasted>"), findings);
return new Report(1, 0, List.of("<pasted>"), findings, "not detected");
}
scan(result.getResult().get(), "<pasted>", findings, locatorStats);
return new Report(1, 1, List.of(), findings, List.of(), List.of(), locatorStats.counts());

scan(result.getResult().get(), "<pasted>", findings);

return new Report(1, 1, List.of(), findings, "not detected");
}

private void scan(CompilationUnit cu, String file, List<Finding> out, LocatorStats locatorStats) {
cu.findAll(MethodCallExpr.class).forEach(locatorStats::scan);
private void scan(CompilationUnit cu, String file, List<Finding> out) {

// MIG-010: page-object candidates with a WebDriver constructor
cu.findAll(ClassOrInterfaceDeclaration.class).stream()
.filter(c -> !c.isInterface())
.filter(c -> c.getConstructors().stream().anyMatch(Analyzer::hasWebDriverParameter))
.forEach(c -> out.add(new Finding("MIG-010", MANUAL, file, line(c), c.getNameAsString(),
"Review as a page object; Selenium Boot documents extending BasePage.")));
// MIG-011: @FindBy fields
cu.findAll(FieldDeclaration.class).forEach(fd -> {
if (fd.getAnnotations().stream().anyMatch(a -> isAnnotation(a.getNameAsString(), "FindBy"))) {
fd.getVariables().forEach(v -> out.add(new Finding("MIG-011", MANUAL, file, line(fd),
v.getNameAsString(),
"Selenium Boot documents By locator fields, not @FindBy; review before mapping to BasePage.")));
}
});
// MIG-012: PageFactory initialization
cu.findAll(MethodCallExpr.class).stream()
.filter(m -> m.getNameAsString().equals("initElements"))
.filter(m -> m.getScope().map(s -> s.toString().endsWith("PageFactory")).orElse(false))
.forEach(m -> out.add(new Finding("MIG-012", MANUAL, file, line(m), "PageFactory.initElements(...)",
"PageFactory is not documented as a Selenium Boot pattern; review page initialization.")));
// MIG-001: ThreadLocal<WebDriver> driver factory
cu.findAll(FieldDeclaration.class).forEach(fd -> {
String type = fd.getElementType().toString();

if (type.startsWith("ThreadLocal<") && type.contains("WebDriver")) {
out.add(new Finding("MIG-001", AUTO, file, line(fd), "ThreadLocal<WebDriver>",
"Delete the driver factory; extend BaseTest (per-thread isolation is built in)."));
}
});
// MIG-002: WebDriverManager.*.setup()

cu.findAll(NameExpr.class).stream()
.filter(n -> n.getNameAsString().equals("WebDriverManager"))
.forEach(n -> out.add(new Finding("MIG-002", AUTO, file, line(n), "WebDriverManager",
"Delete; Selenium Manager fetches drivers automatically.")));
// MIG-003: WebDriverWait / ExpectedConditions
cu.findAll(ExpressionStmt.class).stream()
.filter(Analyzer::containsLegacyWait)
.forEach(statement -> out.add(new Finding("MIG-003", MANUAL, file, line(statement),
"WebDriverWait / ExpectedConditions",

cu.findAll(ObjectCreationExpr.class).stream()
.filter(o -> o.getType().getNameAsString().equals("WebDriverWait"))
.forEach(o -> out.add(new Finding("MIG-003", MANUAL, file, line(o), "new WebDriverWait(...)",
"Use $(locator) auto-wait or getWait(); review the condition by hand.")));
// MIG-004: retry analyzer / annotation transformer

cu.findAll(NameExpr.class).stream()
.filter(n -> n.getNameAsString().equals("ExpectedConditions"))
.forEach(n -> out.add(new Finding("MIG-003", MANUAL, file, line(n), "ExpectedConditions",
"Map to $(locator) auto-wait or a WaitEngine call.")));

cu.findAll(ClassOrInterfaceDeclaration.class).forEach(c -> {

boolean retry = c.getImplementedTypes().stream()
.anyMatch(t -> t.getNameAsString().equals("IRetryAnalyzer")
|| t.getNameAsString().equals("IAnnotationTransformer"));

if (retry) {
out.add(new Finding("MIG-004", AUTO, file, line(c), c.getNameAsString(),
"Delete; set retry.enabled in selenium-boot.yml or use @Retryable."));
}
// MIG-005: screenshot-on-failure listener
boolean listener = c.getImplementedTypes().stream().anyMatch(t -> t.getNameAsString().equals("ITestListener"));
boolean shots = c.findAll(NameExpr.class).stream().anyMatch(n -> n.getNameAsString().equals("TakesScreenshot"))

boolean listener = c.getImplementedTypes().stream()
.anyMatch(t -> t.getNameAsString().equals("ITestListener"));

boolean shots = c.findAll(NameExpr.class).stream()
.anyMatch(n -> n.getNameAsString().equals("TakesScreenshot"))
|| c.toString().contains("TakesScreenshot");

if (listener && shots) {
out.add(new Finding("MIG-005", AUTO, file, line(c), c.getNameAsString(),
"Delete; failure screenshots are captured automatically."));
}
});
// MIG-014: hard-coded sleeps, MIG-016: implicit waits (flag only)

cu.findAll(MethodCallExpr.class).forEach(m -> {
if (m.getNameAsString().equals("sleep") && m.getScope().map(s -> s.toString().equals("Thread")).orElse(false)) {

if (m.getNameAsString().equals("sleep")
&& m.getScope().map(s -> s.toString().equals("Thread")).orElse(false)) {

out.add(new Finding("MIG-014", MANUAL, file, line(m), "Thread.sleep(...)",
"Replace with an auto-waiting locator or getWait()."));
}

if (m.getNameAsString().equals("implicitlyWait")) {
out.add(new Finding("MIG-016", MANUAL, file, line(m), "implicitlyWait(...)",
"Remove; mixing implicit and explicit waits causes flakiness."));
}
});
// MIG-015: custom DriverManager (flag only)

cu.findAll(ClassOrInterfaceDeclaration.class).stream()
.filter(c -> c.getNameAsString().endsWith("DriverManager") || c.getNameAsString().endsWith("DriverFactory"))
.filter(c -> c.getNameAsString().endsWith("DriverManager")
|| c.getNameAsString().endsWith("DriverFactory"))
.forEach(c -> out.add(new Finding("MIG-015", MANUAL, file, line(c), c.getNameAsString(),
"Custom driver lifecycle: review, then replace with BaseTest.")));
// MIG-017: driver lifecycle managed in TestNG or JUnit setup/teardown methods
cu.findAll(MethodDeclaration.class).stream()

Check failure on line 154 in src/main/java/com/seleniumboot/migrator/Analyzer.java

View workflow job for this annotation

GitHub Actions / Unit Tests

cannot find symbol

Check failure on line 154 in src/main/java/com/seleniumboot/migrator/Analyzer.java

View workflow job for this annotation

GitHub Actions / Unit Tests

cannot find symbol
.filter(this::isDriverLifecycleMethod)
.forEach(method -> {
boolean lifecycleOnly = method.getBody().orElseThrow().getStatements().stream()
Expand All @@ -165,7 +164,7 @@
});
}

private boolean isDriverLifecycleMethod(MethodDeclaration method) {

Check failure on line 167 in src/main/java/com/seleniumboot/migrator/Analyzer.java

View workflow job for this annotation

GitHub Actions / Unit Tests

cannot find symbol

Check failure on line 167 in src/main/java/com/seleniumboot/migrator/Analyzer.java

View workflow job for this annotation

GitHub Actions / Unit Tests

cannot find symbol
if (lifecycleAnnotation(method) == null || method.getBody().isEmpty()) return false;
var body = method.getBody().get();
boolean createsDriver = body.findAll(ObjectCreationExpr.class).stream()
Expand All @@ -175,12 +174,12 @@
return createsDriver || quitsDriver;
}

private boolean isLifecycleGlueStatement(Statement statement) {

Check failure on line 177 in src/main/java/com/seleniumboot/migrator/Analyzer.java

View workflow job for this annotation

GitHub Actions / Unit Tests

cannot find symbol

Check failure on line 177 in src/main/java/com/seleniumboot/migrator/Analyzer.java

View workflow job for this annotation

GitHub Actions / Unit Tests

cannot find symbol
if (!(statement instanceof ExpressionStmt expressionStatement)) return false;

Check failure on line 178 in src/main/java/com/seleniumboot/migrator/Analyzer.java

View workflow job for this annotation

GitHub Actions / Unit Tests

cannot find symbol

Check failure on line 178 in src/main/java/com/seleniumboot/migrator/Analyzer.java

View workflow job for this annotation

GitHub Actions / Unit Tests

cannot find symbol
Expression expression = expressionStatement.getExpression();
if (expression instanceof MethodCallExpr call) return isDriverQuit(call);
if (expression instanceof AssignExpr assignment) return isDriverVariable(assignment.getTarget());
if (expression instanceof VariableDeclarationExpr declaration) {

Check failure on line 182 in src/main/java/com/seleniumboot/migrator/Analyzer.java

View workflow job for this annotation

GitHub Actions / Unit Tests

cannot find symbol

Check failure on line 182 in src/main/java/com/seleniumboot/migrator/Analyzer.java

View workflow job for this annotation

GitHub Actions / Unit Tests

cannot find symbol
return declaration.getVariables().stream().allMatch(variable ->
variable.getNameAsString().equals("driver")
&& variable.getInitializer().filter(ObjectCreationExpr.class::isInstance)
Expand All @@ -204,7 +203,7 @@
&& field.getScope() instanceof ThisExpr;
}

private String lifecycleAnnotation(MethodDeclaration method) {

Check failure on line 206 in src/main/java/com/seleniumboot/migrator/Analyzer.java

View workflow job for this annotation

GitHub Actions / Unit Tests

cannot find symbol

Check failure on line 206 in src/main/java/com/seleniumboot/migrator/Analyzer.java

View workflow job for this annotation

GitHub Actions / Unit Tests

cannot find symbol
return method.getAnnotations().stream()
.map(annotation -> annotation.getName().getIdentifier())
.filter(DRIVER_LIFECYCLE_ANNOTATIONS::contains)
Expand All @@ -212,7 +211,7 @@
.orElse(null);
}

private static boolean containsLegacyWait(ExpressionStmt statement) {

Check failure on line 214 in src/main/java/com/seleniumboot/migrator/Analyzer.java

View workflow job for this annotation

GitHub Actions / Unit Tests

cannot find symbol

Check failure on line 214 in src/main/java/com/seleniumboot/migrator/Analyzer.java

View workflow job for this annotation

GitHub Actions / Unit Tests

cannot find symbol
boolean webDriverWait = statement.findAll(ObjectCreationExpr.class).stream()
.anyMatch(creation -> creation.getType().getNameAsString().equals("WebDriverWait"));
boolean expectedConditions = statement.findAll(NameExpr.class).stream()
Expand All @@ -224,65 +223,73 @@
return n.getBegin().map(p -> p.line).orElse(0);
}

private static boolean hasWebDriverParameter(ConstructorDeclaration constructor) {
return constructor.getParameters().stream()
.anyMatch(p -> isWebDriverType(p.getType().asString()));
}
private static String detectTechnologies(Path root) {
Path pom = root.resolve("pom.xml");

private static boolean isWebDriverType(String type) {
return type.equals("WebDriver") || type.endsWith(".WebDriver");
}
if (!Files.exists(pom)) {
return "not detected";
}

private static boolean isAnnotation(String name, String simpleName) {
return name.equals(simpleName) || name.endsWith("." + simpleName);
}
try {
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
Document document = factory.newDocumentBuilder().parse(pom.toFile());

StringBuilder result = new StringBuilder("Maven");

NodeList dependencies = document.getElementsByTagName("dependency");

for (int i = 0; i < dependencies.getLength(); i++) {
Element dependency = (Element) dependencies.item(i);

private static List<String> recognizedTechnologies(List<String> detectedTechnologies) {
boolean testNg = false;
boolean junit4 = false;
boolean junit5 = false;
boolean webDriverManager = false;
boolean extentReports = false;
boolean allure = false;
String seleniumVersion = null;

for (String detected : detectedTechnologies) {
if (!detected.startsWith("Dependency: ")) continue;

String[] coordinates = detected.substring("Dependency: ".length()).split(":", -1);
if (coordinates.length < 2) continue;

String groupId = coordinates[0];
String artifactId = coordinates[1];
String version = coordinates.length > 2 ? coordinates[2] : "";

if (groupId.equals("org.seleniumhq.selenium") && artifactId.equals("selenium-java")) {
seleniumVersion = version;
} else if (groupId.equals("org.testng") && artifactId.equals("testng")) {
testNg = true;
} else if (groupId.equals("junit") && artifactId.equals("junit")) {
junit4 = true;
} else if (groupId.equals("org.junit.jupiter")) {
junit5 = true;
} else if (groupId.equals("io.github.bonigarcia") && artifactId.equals("webdrivermanager")) {
webDriverManager = true;
} else if (groupId.equals("com.aventstack") && artifactId.equals("extentreports")) {
extentReports = true;
} else if (groupId.equals("io.qameta.allure")) {
allure = true;
String groupId = getText(dependency, "groupId");
String artifactId = getText(dependency, "artifactId");
String version = getText(dependency, "version");

if ("org.seleniumhq.selenium".equals(groupId)
&& "selenium-java".equals(artifactId)) {
result.append(", Selenium ").append(version);
}

if ("org.testng".equals(groupId)) {
result.append(", TestNG");
}

if ("junit".equals(groupId) && "junit".equals(artifactId)) {
result.append(", JUnit 4");
}

if ("org.junit.jupiter".equals(groupId)) {
result.append(", JUnit 5");
}

if ("io.github.bonigarcia".equals(groupId)
&& "webdrivermanager".equals(artifactId)) {
result.append(", WebDriverManager");
}

if ("com.aventstack".equals(groupId)) {
result.append(", ExtentReports");
}

if ("io.qameta.allure".equals(groupId)) {
result.append(", Allure");
}
}

return result.toString();

} catch (Exception e) {
return "Maven";
}
}

private static String getText(Element parent, String tagName) {
NodeList nodes = parent.getElementsByTagName(tagName);

List<String> recognized = new ArrayList<>();
if (seleniumVersion != null) {
recognized.add(seleniumVersion.isBlank() ? "Selenium" : "Selenium " + seleniumVersion);
if (nodes.getLength() == 0) {
return "";
}
if (testNg) recognized.add("TestNG");
if (junit4) recognized.add("JUnit 4");
if (junit5) recognized.add("JUnit 5");
if (webDriverManager) recognized.add("WebDriverManager");
if (extentReports) recognized.add("ExtentReports");
if (allure) recognized.add("Allure");
return List.copyOf(recognized);

return nodes.item(0).getTextContent().trim();
}
}
38 changes: 5 additions & 33 deletions src/main/java/com/seleniumboot/migrator/Report.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,7 @@
import java.util.Map;
import java.util.TreeMap;


public record Report(int filesFound, int filesParsed, List<String> unparsable, List<Finding> findings,
List<String> detectedTechnologies, List<String> recognizedTechnologies,
Map<String, Long> locatorCounts) {

public Report(int filesFound, int filesParsed, List<String> unparsable, List<Finding> findings) {
this(filesFound, filesParsed, unparsable, findings, List.of(), List.of(), Map.of());
}

public Report(int filesFound, int filesParsed, List<String> unparsable, List<Finding> findings,
List<String> detectedTechnologies) {
this(filesFound, filesParsed, unparsable, findings, detectedTechnologies, List.of(), Map.of());
}
public record Report(int filesFound, int filesParsed, List<String> unparsable, List<Finding> findings, String technologies) {

public long count(Finding.Status s) {
return findings.stream().filter(f -> f.status() == s).count();
Expand Down Expand Up @@ -55,26 +43,10 @@ public String render() {
StringBuilder sb = new StringBuilder("Selenium Boot Migration Analysis\n\n");
sb.append(String.format("Files found: %d%n", filesFound));
sb.append(String.format("Files parsed: %d%n", filesParsed));
sb.append("\nDetected technologies\n");
if (detectedTechnologies.isEmpty()) {
sb.append(" No supported build descriptor found\n");
} else {
detectedTechnologies.forEach(technology -> sb.append(" ").append(technology).append('\n'));
}
sb.append("\nRecognized technologies\n");
if (recognizedTechnologies.isEmpty()) {
sb.append(" No supported test technologies detected\n");
} else {
sb.append(" ").append(String.join(", ", recognizedTechnologies)).append('\n');
}
sb.append("\nLocator usage:\n");
if (locatorCounts.isEmpty()) {
sb.append(" None detected\n");
} else {
locatorCounts.forEach((locator, count) ->
sb.append(String.format(" %-22s %d%n", locator + ":", count)));
}
ruleCounts().forEach((r, n) -> sb.append(String.format("%-40s %d%n", ruleLabel(r) + ":", n)));
sb.append(String.format("%nDetected technologies: %s%n", technologies));
Map<String, Long> byRule = new TreeMap<>();
findings.forEach(f -> byRule.merge(f.ruleId(), 1L, Long::sum));
byRule.forEach((r, n) -> sb.append(String.format("%-40s %d%n", ruleLabel(r) + ":", n)));
sb.append(String.format("%nMaps cleanly: %d%n", count(Finding.Status.AUTO)));
sb.append(String.format("Manual review required: %d%n", count(Finding.Status.MANUAL)));
sb.append(String.format("Unparsable files: %d%n", unparsable.size()));
Expand Down
Loading
Loading