From 55bbe41aec9ed2da0097fb716d355b5779588dfd Mon Sep 17 00:00:00 2001 From: Igor Kirillov <63199449+KirillovProj@users.noreply.github.com> Date: Tue, 25 Nov 2025 13:56:58 +0100 Subject: [PATCH 1/4] Add javaparser --- build.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/build.gradle b/build.gradle index 1e210c2..6a30548 100644 --- a/build.gradle +++ b/build.gradle @@ -27,6 +27,7 @@ dependencies { api 'org.assertj:assertj-swing-junit:3.17.1' api 'org.apache.httpcomponents:httpclient:4.5.14' api 'com.google.code.gson:gson:2.10.1' + api 'com.github.javaparser:javaparser-core:3.25.5' compileOnly 'org.projectlombok:lombok:1.18.30' annotationProcessor 'org.projectlombok:lombok:1.18.30' From c198e3c188a30573571004c1acf210050436b040 Mon Sep 17 00:00:00 2001 From: Igor Kirillov <63199449+KirillovProj@users.noreply.github.com> Date: Tue, 25 Nov 2025 13:58:30 +0100 Subject: [PATCH 2/4] Create ExitCallDetector.java --- .../hstest/common/ExitCallDetector.java | 201 ++++++++++++++++++ 1 file changed, 201 insertions(+) create mode 100644 src/main/java/org/hyperskill/hstest/common/ExitCallDetector.java diff --git a/src/main/java/org/hyperskill/hstest/common/ExitCallDetector.java b/src/main/java/org/hyperskill/hstest/common/ExitCallDetector.java new file mode 100644 index 0000000..2405dca --- /dev/null +++ b/src/main/java/org/hyperskill/hstest/common/ExitCallDetector.java @@ -0,0 +1,201 @@ +package org.hyperskill.hstest.common; + +import com.github.javaparser.JavaParser; +import com.github.javaparser.ParseResult; +import com.github.javaparser.ast.CompilationUnit; +import com.github.javaparser.ast.expr.MethodCallExpr; +import com.github.javaparser.ast.visitor.VoidVisitorAdapter; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * Detects forbidden exit calls in user code that could terminate the JVM. + * This includes System.exit(), exitProcess(), Runtime.exit(), and Runtime.halt(). + */ +public class ExitCallDetector { + + /** + * Result of the exit call detection + */ + public static class DetectionResult { + private final boolean hasExitCalls; + private final List violations; + + public DetectionResult(boolean hasExitCalls, List violations) { + this.hasExitCalls = hasExitCalls; + this.violations = violations; + } + + public boolean hasExitCalls() { + return hasExitCalls; + } + + public List getViolations() { + return violations; + } + + public String getFormattedMessage() { + if (!hasExitCalls) { + return null; + } + StringBuilder sb = new StringBuilder(); + sb.append("Your code contains forbidden exit calls that would terminate the test execution:\n\n"); + for (String violation : violations) { + sb.append(" ").append(violation).append("\n"); + } + sb.append("\nPlease remove all System.exit(), exitProcess(), Runtime.exit(), and Runtime.halt() calls from your code."); + return sb.toString(); + } + } + + /** + * Analyzes a single Java source file for exit calls + */ + public static DetectionResult analyzeFile(File file) throws IOException { + String content = Files.readString(file.toPath()); + return analyzeSourceCode(content, file.getName()); + } + + /** + * Analyzes Java source code string for exit calls + */ + public static DetectionResult analyzeSourceCode(String sourceCode, String fileName) { + List violations = new ArrayList<>(); + + // First, do a simple string-based check as a fast pre-filter + if (!containsSimpleExitPattern(sourceCode)) { + return new DetectionResult(false, violations); + } + + // If simple check finds something, do detailed AST analysis + try { + JavaParser parser = new JavaParser(); + ParseResult parseResult = parser.parse(sourceCode); + + if (parseResult.isSuccessful() && parseResult.getResult().isPresent()) { + CompilationUnit cu = parseResult.getResult().get(); + ExitCallVisitor visitor = new ExitCallVisitor(fileName); + visitor.visit(cu, violations); + } + } catch (Exception e) { + // If parsing fails, fall back to simple string check + violations.addAll(simpleStringAnalysis(sourceCode, fileName)); + } + + return new DetectionResult(!violations.isEmpty(), violations); + } + + /** + * Analyzes all Java files in a directory recursively + */ + public static DetectionResult analyzeDirectory(Path directory) throws IOException { + List allViolations = new ArrayList<>(); + + try (Stream paths = Files.walk(directory)) { + List javaFiles = paths + .filter(Files::isRegularFile) + .filter(p -> p.toString().endsWith(".java") || p.toString().endsWith(".kt")) + .collect(Collectors.toList()); + + for (Path path : javaFiles) { + DetectionResult result = analyzeFile(path.toFile()); + if (result.hasExitCalls()) { + allViolations.addAll(result.getViolations()); + } + } + } + + return new DetectionResult(!allViolations.isEmpty(), allViolations); + } + + /** + * Fast string-based pre-filter to avoid expensive AST parsing when not needed + */ + private static boolean containsSimpleExitPattern(String sourceCode) { + return sourceCode.contains("exit") || sourceCode.contains("halt"); + } + + /** + * Simple string-based analysis as fallback + */ + private static List simpleStringAnalysis(String sourceCode, String fileName) { + List violations = new ArrayList<>(); + String[] lines = sourceCode.split("\n"); + + for (int i = 0; i < lines.length; i++) { + String line = lines[i].trim(); + + // Skip comments + if (line.startsWith("//") || line.startsWith("/*") || line.startsWith("*")) { + continue; + } + + if (line.contains("System.exit")) { + violations.add(fileName + " (line " + (i + 1) + "): System.exit() call detected"); + } + if (line.contains("exitProcess")) { + violations.add(fileName + " (line " + (i + 1) + "): exitProcess() call detected"); + } + if (line.contains("Runtime") && line.contains(".exit")) { + violations.add(fileName + " (line " + (i + 1) + "): Runtime.exit() call detected"); + } + if (line.contains("Runtime") && line.contains(".halt")) { + violations.add(fileName + " (line " + (i + 1) + "): Runtime.halt() call detected"); + } + } + + return violations; + } + + /** + * AST visitor to find method calls + */ + private static class ExitCallVisitor extends VoidVisitorAdapter> { + private final String fileName; + + public ExitCallVisitor(String fileName) { + this.fileName = fileName; + } + + @Override + public void visit(MethodCallExpr methodCall, List violations) { + super.visit(methodCall, violations); + + String methodName = methodCall.getNameAsString(); + + // Check for exit, exitProcess, or halt calls + if (methodName.equals("exit") || methodName.equals("exitProcess") || methodName.equals("halt")) { + + // Check if it's System.exit() + if (methodCall.getScope().isPresent()) { + String scope = methodCall.getScope().get().toString(); + + if (scope.equals("System")) { + int line = methodCall.getBegin().map(pos -> pos.line).orElse(0); + violations.add(fileName + " (line " + line + "): System.exit() call detected"); + } + else if (scope.contains("Runtime")) { + int line = methodCall.getBegin().map(pos -> pos.line).orElse(0); + if (methodName.equals("exit")) { + violations.add(fileName + " (line " + line + "): Runtime.exit() call detected"); + } else if (methodName.equals("halt")) { + violations.add(fileName + " (line " + line + "): Runtime.halt() call detected"); + } + } + } + // Kotlin's exitProcess() has no scope + else if (methodName.equals("exitProcess")) { + int line = methodCall.getBegin().map(pos -> pos.line).orElse(0); + violations.add(fileName + " (line " + line + "): exitProcess() call detected"); + } + } + } + } +} From e761bd738fe337b02e5ae51824cd963bcb9df1d8 Mon Sep 17 00:00:00 2001 From: Igor Kirillov <63199449+KirillovProj@users.noreply.github.com> Date: Tue, 25 Nov 2025 13:59:36 +0100 Subject: [PATCH 3/4] Check for exit calls in StageTest --- .../hyperskill/hstest/stage/StageTest.java | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/src/main/java/org/hyperskill/hstest/stage/StageTest.java b/src/main/java/org/hyperskill/hstest/stage/StageTest.java index c398282..93b8552 100644 --- a/src/main/java/org/hyperskill/hstest/stage/StageTest.java +++ b/src/main/java/org/hyperskill/hstest/stage/StageTest.java @@ -2,6 +2,7 @@ import lombok.Getter; import org.hyperskill.hstest.checker.CheckLibraryVersion; +import org.hyperskill.hstest.common.ExitCallDetector; import org.hyperskill.hstest.common.FileUtils; import org.hyperskill.hstest.common.ReflectionUtils; import org.hyperskill.hstest.dynamic.ClassSearcher; @@ -22,7 +23,9 @@ import org.junit.runner.JUnitCore; import org.junit.runner.Result; +import java.io.IOException; import java.lang.reflect.Modifier; +import java.nio.file.Path; import java.util.ArrayList; import java.util.List; import java.util.stream.Collectors; @@ -137,6 +140,28 @@ private void printTestNum(int num) { OutputHandler.print(RED_BOLD + "\nStart test " + num + totalTests + RESET); } + /** + * Checks user code for forbidden exit calls before running tests + */ + private void checkForExitCalls() { + // Only check Java files (other languages handled differently in Docker) + if (!hasJavaSolution(FileUtils.cwd())) { + return; + } + + try { + Path currentDir = FileUtils.cwd().toPath(); + ExitCallDetector.DetectionResult result = ExitCallDetector.analyzeDirectory(currentDir); + + if (result.hasExitCalls()) { + throw new WrongAnswer(result.getFormattedMessage()); + } + } catch (IOException e) { + // If we can't read files, just continue (fail safely) + // The SecurityManager will catch it at runtime if needed + } + } + @Test public final void start() { int currTest = 0; @@ -149,6 +174,9 @@ public final void start() { ReflectionUtils.setupCwd(this); } + // Check for exit calls before running any tests + checkForExitCalls(); + List testRuns = initTests(); for (TestRun testRun : testRuns) { From ac9d74db6de68d06c8093f8e5c6af881596f7b61 Mon Sep 17 00:00:00 2001 From: Igor Kirillov <63199449+KirillovProj@users.noreply.github.com> Date: Tue, 25 Nov 2025 14:04:36 +0100 Subject: [PATCH 4/4] fix --- src/main/java/org/hyperskill/hstest/stage/StageTest.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/hyperskill/hstest/stage/StageTest.java b/src/main/java/org/hyperskill/hstest/stage/StageTest.java index 93b8552..06b62d2 100644 --- a/src/main/java/org/hyperskill/hstest/stage/StageTest.java +++ b/src/main/java/org/hyperskill/hstest/stage/StageTest.java @@ -23,6 +23,7 @@ import org.junit.runner.JUnitCore; import org.junit.runner.Result; +import java.io.File; import java.io.IOException; import java.lang.reflect.Modifier; import java.nio.file.Path; @@ -150,7 +151,7 @@ private void checkForExitCalls() { } try { - Path currentDir = FileUtils.cwd().toPath(); + Path currentDir = new File(FileUtils.cwd()).toPath(); ExitCallDetector.DetectionResult result = ExitCallDetector.analyzeDirectory(currentDir); if (result.hasExitCalls()) {