Skip to content
Merged
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
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ java -jar target/selenium-boot-migrator.jar analyze ./my-selenium-project
```

Output is a count per rule, what maps cleanly vs. needs review, and an *estimated* confidence.
The estimate is a guide, not a guarantee.
The estimate is a guide, not a guarantee. The report also lists dependencies found in Maven
`pom.xml` files and Gradle `build.gradle` / `build.gradle.kts` files. Gradle files are inspected
as text; a Gradle installation is not required.

## Rules

Expand Down
2 changes: 1 addition & 1 deletion src/main/java/com/seleniumboot/migrator/Analyzer.java
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ public Report analyze(Path root) throws IOException {
parsed++;
scan(result.getResult().get(), root.relativize(f).toString(), findings);
}
return new Report(files.size(), parsed, unparsable, findings);
return new Report(files.size(), parsed, unparsable, findings, BuildFileAnalyzer.detect(root));
}

/** Analyze a single pasted source string. */
Expand Down
145 changes: 145 additions & 0 deletions src/main/java/com/seleniumboot/migrator/BuildFileAnalyzer.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
package com.seleniumboot.migrator;

import org.w3c.dom.Element;
import org.w3c.dom.Node;

import javax.xml.XMLConstants;
import javax.xml.parsers.DocumentBuilderFactory;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.FileVisitResult;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

final class BuildFileAnalyzer {

private static final Set<String> IGNORED_DIRECTORIES = Set.of("target", "build", ".gradle", "node_modules");
private static final Pattern GRADLE_DEPENDENCY = Pattern.compile(
"(?m)^\\s*(implementation|api|compileOnly|runtimeOnly|testImplementation|testCompileOnly|"
+ "testRuntimeOnly|classpath|annotationProcessor|kapt)\\s*(?:\\(\\s*)?['\\\"]([^'\\\"]+)['\\\"]");

private BuildFileAnalyzer() { }

static List<String> detect(Path root) throws IOException {
List<String> detected = new ArrayList<>();
Set<String> dependencies = new LinkedHashSet<>();
List<Path> buildFiles = findBuildFiles(root);

for (Path buildFile : buildFiles) {
String name = buildFile.getFileName().toString();
if (name.equals("pom.xml")) {
try {
dependencies.addAll(mavenDependencies(buildFile));
detected.add("Build system: Maven");
} catch (IOException exception) {
detected.add("Build system: Maven (could not parse pom.xml)");
}
} else if (name.equals("build.gradle")) {
detected.add("Build system: Gradle (Groovy DSL)");
dependencies.addAll(gradleDependencies(buildFile));
} else {
detected.add("Build system: Gradle (Kotlin DSL)");
dependencies.addAll(gradleDependencies(buildFile));
}
}

dependencies.stream().sorted().map(dependency -> "Dependency: " + dependency).forEach(detected::add);
return List.copyOf(detected);
}

private static List<Path> findBuildFiles(Path root) throws IOException {
List<Path> buildFiles = new ArrayList<>();
Files.walkFileTree(root, new SimpleFileVisitor<>() {
@Override
public FileVisitResult preVisitDirectory(Path directory, BasicFileAttributes attributes) {
if (!directory.equals(root) && IGNORED_DIRECTORIES.contains(directory.getFileName().toString())) {
return FileVisitResult.SKIP_SUBTREE;
}
return FileVisitResult.CONTINUE;
}

@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attributes) {
if (attributes.isRegularFile() && isBuildFile(file.getFileName().toString())) {
buildFiles.add(file);
}
return FileVisitResult.CONTINUE;
}
});
buildFiles.sort(Path::compareTo);
return buildFiles;
}

private static boolean isBuildFile(String name) {
return name.equals("pom.xml") || name.equals("build.gradle") || name.equals("build.gradle.kts");
}

private static List<String> gradleDependencies(Path buildFile) throws IOException {
String source = Files.readString(buildFile);
Matcher matcher = GRADLE_DEPENDENCY.matcher(source);
List<String> dependencies = new ArrayList<>();
while (matcher.find()) {
String coordinate = matcher.group(2).trim();
if (coordinate.matches("[^\\s:]+:[^\\s:]+:[^\\s:]+(?::[^\\s:]+)?")) {
dependencies.add(coordinate);
}
}
return dependencies;
}

private static List<String> mavenDependencies(Path pom) throws IOException {
try {
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
factory.setNamespaceAware(true);
factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
factory.setFeature("http://xml.org/sax/features/external-general-entities", false);
factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
factory.setXIncludeAware(false);
factory.setExpandEntityReferences(false);

var document = factory.newDocumentBuilder().parse(pom.toFile());
Element project = document.getDocumentElement();
Element dependencies = directChild(project, "dependencies");
if (dependencies == null) return List.of();

List<String> coordinates = new ArrayList<>();
for (Node node = dependencies.getFirstChild(); node != null; node = node.getNextSibling()) {
if (node instanceof Element dependency && localName(dependency).equals("dependency")) {
String group = childText(dependency, "groupId");
String artifact = childText(dependency, "artifactId");
String version = childText(dependency, "version");
if (!group.isBlank() && !artifact.isBlank()) {
coordinates.add(group + ":" + artifact + (version.isBlank() ? "" : ":" + version));
}
}
}
return coordinates;
} catch (Exception exception) {
throw new IOException("Could not parse Maven build file: " + pom, exception);
}
}

private static Element directChild(Element parent, String name) {
for (Node node = parent.getFirstChild(); node != null; node = node.getNextSibling()) {
if (node instanceof Element element && localName(element).equals(name)) return element;
}
return null;
}

private static String childText(Element parent, String name) {
Element child = directChild(parent, name);
return child == null ? "" : child.getTextContent().trim();
}

private static String localName(Element element) {
return element.getLocalName() == null ? element.getTagName() : element.getLocalName();
}
}
13 changes: 12 additions & 1 deletion src/main/java/com/seleniumboot/migrator/Report.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,12 @@
import java.util.Map;
import java.util.TreeMap;

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

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

public long count(Finding.Status s) {
return findings.stream().filter(f -> f.status() == s).count();
Expand All @@ -22,6 +27,12 @@ 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'));
}
Map<String, Long> byRule = new TreeMap<>();
findings.forEach(f -> byRule.merge(f.ruleId(), 1L, Long::sum));
byRule.forEach((r, n) -> sb.append(String.format("%-25s %d%n", r + ":", n)));
Expand Down
60 changes: 60 additions & 0 deletions src/test/java/com/seleniumboot/migrator/AnalyzerTest.java
Original file line number Diff line number Diff line change
@@ -1,13 +1,21 @@
package com.seleniumboot.migrator;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;

import java.net.URISyntaxException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;

import static org.junit.jupiter.api.Assertions.*;

class AnalyzerTest {

private static Path fixture(String name) throws URISyntaxException {
return Path.of(AnalyzerTest.class.getResource("/" + name).toURI());
}

private static List<String> rules(String src) {
return new Analyzer().analyzeSource(src).findings().stream().map(Finding::ruleId).toList();
}
Expand Down Expand Up @@ -57,4 +65,56 @@ void unparsableSourceIsReportedNotThrown() {
var report = new Analyzer().analyzeSource("class {{{");
assertEquals(1, report.unparsable().size());
}

@Test
void detectsGroovyGradleBuildAndDependencies() throws Exception {
String output = new Analyzer().analyze(fixture("gradle-groovy")).render();
assertTrue(output.contains("Detected technologies"));
assertTrue(output.contains("Build system: Gradle (Groovy DSL)"));
assertTrue(output.contains("Dependency: org.seleniumhq.selenium:selenium-java:4.21.0"));
assertTrue(output.contains("Dependency: org.testng:testng:7.10.2"));
}

@Test
void detectsKotlinGradleBuildAndDependencies() throws Exception {
String output = new Analyzer().analyze(fixture("gradle-kotlin")).render();
assertTrue(output.contains("Detected technologies"));
assertTrue(output.contains("Build system: Gradle (Kotlin DSL)"));
assertTrue(output.contains("Dependency: org.seleniumhq.selenium:selenium-java:4.21.0"));
assertTrue(output.contains("Dependency: org.junit.jupiter:junit-jupiter:5.10.2"));
}

@Test
void reportsMavenDependenciesInTheSameSection() throws Exception {
String output = new Analyzer().analyze(fixture("maven")).render();
assertTrue(output.contains("Detected technologies"));
assertTrue(output.contains("Build system: Maven"));
assertTrue(output.contains("Dependency: org.seleniumhq.selenium:selenium-java:4.21.0"));
}

@Test
void reportsMalformedMavenBuildAndContinuesAnalysis(@TempDir Path root) throws Exception {
Files.writeString(root.resolve("pom.xml"), "<project>");
Files.writeString(root.resolve("build.gradle"), "implementation 'org.example:sample:1.0'");

String output = new Analyzer().analyze(root).render();

assertTrue(output.contains("Build system: Maven (could not parse pom.xml)"));
assertTrue(output.contains("Build system: Gradle (Groovy DSL)"));
assertTrue(output.contains("Dependency: org.example:sample:1.0"));
}

@Test
void ignoresBuildFilesInGeneratedAndVendorDirectories(@TempDir Path root) throws Exception {
for (String directory : List.of("target", "build", ".gradle", "node_modules")) {
Path ignored = Files.createDirectories(root.resolve(directory));
Files.writeString(ignored.resolve("build.gradle"), "implementation 'org.example:sample:1.0'");
}

String output = new Analyzer().analyze(root).render();

assertFalse(output.contains("Build system: Gradle"));
assertFalse(output.contains("Dependency: org.example:sample:1.0"));
assertTrue(output.contains("No supported build descriptor found"));
}
}
8 changes: 8 additions & 0 deletions src/test/resources/gradle-groovy/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
plugins {
id 'java'
}

dependencies {
implementation 'org.seleniumhq.selenium:selenium-java:4.21.0'
testImplementation 'org.testng:testng:7.10.2'
}
8 changes: 8 additions & 0 deletions src/test/resources/gradle-kotlin/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
plugins {
java
}

dependencies {
implementation("org.seleniumhq.selenium:selenium-java:4.21.0")
testImplementation("org.junit.jupiter:junit-jupiter:5.10.2")
}
13 changes: 13 additions & 0 deletions src/test/resources/maven/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<project xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<groupId>example</groupId>
<artifactId>selenium-fixture</artifactId>
<version>1.0.0</version>
<dependencies>
<dependency>
<groupId>org.seleniumhq.selenium</groupId>
<artifactId>selenium-java</artifactId>
<version>4.21.0</version>
</dependency>
</dependencies>
</project>