From e08655c15c8046e51efb7cb64d85f3d1f8c50e41 Mon Sep 17 00:00:00 2001 From: Pushpak-Jaiswal Date: Sat, 26 Sep 2026 11:33:03 +0530 Subject: [PATCH 1/2] feat: gradle-support --- README.md | 4 +- .../com/seleniumboot/migrator/Analyzer.java | 2 +- .../migrator/BuildFileAnalyzer.java | 121 ++++++++++++++++++ .../com/seleniumboot/migrator/Report.java | 13 +- .../seleniumboot/migrator/AnalyzerTest.java | 32 +++++ src/test/resources/gradle-groovy/build.gradle | 8 ++ .../resources/gradle-kotlin/build.gradle.kts | 8 ++ src/test/resources/maven/pom.xml | 13 ++ 8 files changed, 198 insertions(+), 3 deletions(-) create mode 100644 src/main/java/com/seleniumboot/migrator/BuildFileAnalyzer.java create mode 100644 src/test/resources/gradle-groovy/build.gradle create mode 100644 src/test/resources/gradle-kotlin/build.gradle.kts create mode 100644 src/test/resources/maven/pom.xml diff --git a/README.md b/README.md index 0342637..ef8f154 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/src/main/java/com/seleniumboot/migrator/Analyzer.java b/src/main/java/com/seleniumboot/migrator/Analyzer.java index e6594dc..3a0ecff 100644 --- a/src/main/java/com/seleniumboot/migrator/Analyzer.java +++ b/src/main/java/com/seleniumboot/migrator/Analyzer.java @@ -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. */ diff --git a/src/main/java/com/seleniumboot/migrator/BuildFileAnalyzer.java b/src/main/java/com/seleniumboot/migrator/BuildFileAnalyzer.java new file mode 100644 index 0000000..4682cb5 --- /dev/null +++ b/src/main/java/com/seleniumboot/migrator/BuildFileAnalyzer.java @@ -0,0 +1,121 @@ +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.Path; +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; +import java.util.stream.Stream; + +final class BuildFileAnalyzer { + + 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 detect(Path root) throws IOException { + List detected = new ArrayList<>(); + Set dependencies = new LinkedHashSet<>(); + List buildFiles; + try (Stream paths = Files.walk(root)) { + buildFiles = paths.filter(Files::isRegularFile) + .filter(path -> isBuildFile(path.getFileName().toString())) + .sorted() + .toList(); + } + + for (Path buildFile : buildFiles) { + String name = buildFile.getFileName().toString(); + if (name.equals("pom.xml")) { + detected.add("Build system: Maven"); + dependencies.addAll(mavenDependencies(buildFile)); + } 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 boolean isBuildFile(String name) { + return name.equals("pom.xml") || name.equals("build.gradle") || name.equals("build.gradle.kts"); + } + + private static List gradleDependencies(Path buildFile) throws IOException { + String source = Files.readString(buildFile); + Matcher matcher = GRADLE_DEPENDENCY.matcher(source); + List 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 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 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(); + } +} \ No newline at end of file diff --git a/src/main/java/com/seleniumboot/migrator/Report.java b/src/main/java/com/seleniumboot/migrator/Report.java index 6020c0e..f202b95 100644 --- a/src/main/java/com/seleniumboot/migrator/Report.java +++ b/src/main/java/com/seleniumboot/migrator/Report.java @@ -4,7 +4,12 @@ import java.util.Map; import java.util.TreeMap; -public record Report(int filesFound, int filesParsed, List unparsable, List findings) { +public record Report(int filesFound, int filesParsed, List unparsable, List findings, + List detectedTechnologies) { + + public Report(int filesFound, int filesParsed, List unparsable, List findings) { + this(filesFound, filesParsed, unparsable, findings, List.of()); + } public long count(Finding.Status s) { return findings.stream().filter(f -> f.status() == s).count(); @@ -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 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))); diff --git a/src/test/java/com/seleniumboot/migrator/AnalyzerTest.java b/src/test/java/com/seleniumboot/migrator/AnalyzerTest.java index 0c24e49..bb12c28 100644 --- a/src/test/java/com/seleniumboot/migrator/AnalyzerTest.java +++ b/src/test/java/com/seleniumboot/migrator/AnalyzerTest.java @@ -2,12 +2,18 @@ import org.junit.jupiter.api.Test; +import java.net.URISyntaxException; +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 rules(String src) { return new Analyzer().analyzeSource(src).findings().stream().map(Finding::ruleId).toList(); } @@ -57,4 +63,30 @@ 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")); + } } diff --git a/src/test/resources/gradle-groovy/build.gradle b/src/test/resources/gradle-groovy/build.gradle new file mode 100644 index 0000000..063da68 --- /dev/null +++ b/src/test/resources/gradle-groovy/build.gradle @@ -0,0 +1,8 @@ +plugins { + id 'java' +} + +dependencies { + implementation 'org.seleniumhq.selenium:selenium-java:4.21.0' + testImplementation 'org.testng:testng:7.10.2' +} \ No newline at end of file diff --git a/src/test/resources/gradle-kotlin/build.gradle.kts b/src/test/resources/gradle-kotlin/build.gradle.kts new file mode 100644 index 0000000..4a4d471 --- /dev/null +++ b/src/test/resources/gradle-kotlin/build.gradle.kts @@ -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") +} \ No newline at end of file diff --git a/src/test/resources/maven/pom.xml b/src/test/resources/maven/pom.xml new file mode 100644 index 0000000..fc3ac24 --- /dev/null +++ b/src/test/resources/maven/pom.xml @@ -0,0 +1,13 @@ + + 4.0.0 + example + selenium-fixture + 1.0.0 + + + org.seleniumhq.selenium + selenium-java + 4.21.0 + + + \ No newline at end of file From 575896f8678a70cb5b85dc5058f92dfe87a82886 Mon Sep 17 00:00:00 2001 From: Pushpak-Jaiswal Date: Sat, 26 Sep 2026 21:28:36 +0530 Subject: [PATCH 2/2] Requested Updates and Added Tests --- .../migrator/BuildFileAnalyzer.java | 44 ++++++++++++++----- .../seleniumboot/migrator/AnalyzerTest.java | 28 ++++++++++++ 2 files changed, 62 insertions(+), 10 deletions(-) diff --git a/src/main/java/com/seleniumboot/migrator/BuildFileAnalyzer.java b/src/main/java/com/seleniumboot/migrator/BuildFileAnalyzer.java index 4682cb5..4cbf125 100644 --- a/src/main/java/com/seleniumboot/migrator/BuildFileAnalyzer.java +++ b/src/main/java/com/seleniumboot/migrator/BuildFileAnalyzer.java @@ -7,17 +7,20 @@ 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; -import java.util.stream.Stream; final class BuildFileAnalyzer { + private static final Set 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*)?['\\\"]([^'\\\"]+)['\\\"]"); @@ -27,19 +30,17 @@ private BuildFileAnalyzer() { } static List detect(Path root) throws IOException { List detected = new ArrayList<>(); Set dependencies = new LinkedHashSet<>(); - List buildFiles; - try (Stream paths = Files.walk(root)) { - buildFiles = paths.filter(Files::isRegularFile) - .filter(path -> isBuildFile(path.getFileName().toString())) - .sorted() - .toList(); - } + List buildFiles = findBuildFiles(root); for (Path buildFile : buildFiles) { String name = buildFile.getFileName().toString(); if (name.equals("pom.xml")) { - detected.add("Build system: Maven"); - dependencies.addAll(mavenDependencies(buildFile)); + 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)); @@ -53,6 +54,29 @@ static List detect(Path root) throws IOException { return List.copyOf(detected); } + private static List findBuildFiles(Path root) throws IOException { + List 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"); } diff --git a/src/test/java/com/seleniumboot/migrator/AnalyzerTest.java b/src/test/java/com/seleniumboot/migrator/AnalyzerTest.java index bb12c28..219e675 100644 --- a/src/test/java/com/seleniumboot/migrator/AnalyzerTest.java +++ b/src/test/java/com/seleniumboot/migrator/AnalyzerTest.java @@ -1,8 +1,10 @@ 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; @@ -89,4 +91,30 @@ void reportsMavenDependenciesInTheSameSection() throws Exception { 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"), ""); + 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")); + } }