From 17cdd8117aeb15aabfd31e0bdfa8317a5f3d160e Mon Sep 17 00:00:00 2001 From: zir0 Date: Sun, 19 Apr 2026 01:20:18 +0300 Subject: [PATCH 1/2] Expose config files from ProjectFiles --- pom.xml | 2 +- .../hadi/clarpse/compiler/ProjectFiles.java | 68 +++++++++++++++++-- .../java/com/hadi/test/ProjectFilesTest.java | 44 ++++++++++++ 3 files changed, 107 insertions(+), 7 deletions(-) diff --git a/pom.xml b/pom.xml index cc456d08..2e7efdb0 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ io.github.hadi-technology clarpse - 9.5.2 + 9.5.3 jar ${project.groupId}:${project.artifactId} diff --git a/src/main/java/com/hadi/clarpse/compiler/ProjectFiles.java b/src/main/java/com/hadi/clarpse/compiler/ProjectFiles.java index dca94943..3ba5e93b 100644 --- a/src/main/java/com/hadi/clarpse/compiler/ProjectFiles.java +++ b/src/main/java/com/hadi/clarpse/compiler/ProjectFiles.java @@ -178,11 +178,9 @@ private void initFilesFromDir(File projectFiles) throws IOException { Iterator it = FileUtils.iterateFiles(projectFiles, null, true); while (it.hasNext()) { File nextFile = it.next(); - if (nextFile.isFile() && Lang.langFromExtn(FilenameUtils.getExtension(nextFile.getName())) != null) { - this.insertFile(new ProjectFile( - nextFile.getAbsolutePath(), - FileUtils.readFileToString(nextFile, StandardCharsets.UTF_8)) - ); + if (nextFile.isFile()) { + String content = FileUtils.readFileToString(nextFile, StandardCharsets.UTF_8); + this.insertFile(new ProjectFile(nextFile.getAbsolutePath(), content)); } } LOGGER.info("Read " + this.size + " files."); @@ -309,6 +307,8 @@ public final void insertFile(final ProjectFile file) { Lang fileLang = Lang.langFromExtn(file.extension()); if (fileLang != null) { this.insertFile(file, fileLang); + } else if (isConfigFile(file.path())) { + this.insertConfigFile(file); } else { LOGGER.debug("Skipping file: " + file.path() + "."); } @@ -325,6 +325,12 @@ private void insertFile(final ProjectFile file, Lang lang) { LOGGER.debug("Inserted file " + file + "."); } + private void insertConfigFile(final ProjectFile file) { + this.configFiles.put(normalizeConfigPath(file.path()), file.content()); + this.size += 1; + LOGGER.debug("Inserted config file " + file + "."); + } + /** * Removes a file from this ProjectFiles instance by path. * @@ -342,8 +348,15 @@ public boolean removeFile(final String path) { if (removedCount > 0) { this.size -= removedCount; LOGGER.debug("Removed " + removedCount + " file(s) at path: " + path + "."); + return true; } - return removedCount > 0; + String normalizedConfigPath = normalizeConfigPath(path); + if (this.configFiles.remove(normalizedConfigPath) != null) { + this.size -= 1; + LOGGER.debug("Removed config file at path: " + normalizedConfigPath + "."); + return true; + } + return false; } /** @@ -366,6 +379,7 @@ public final Collection files(Lang language) { public final Collection files() { Set allFiles = new HashSet<>(); this.langToFilesMap.forEach((lang, files) -> allFiles.addAll(files)); + this.configFiles.forEach((path, content) -> allFiles.add(new ProjectFile(path, content))); return Set.copyOf(allFiles); } @@ -460,6 +474,48 @@ public Set matchingFilesByName(String matchName) { Set result = new HashSet<>(); this.langToFilesMap.forEach((lang, files) -> result.addAll(files.stream().filter( file -> file.name().equals(matchName)).collect(Collectors.toList()))); + this.configFiles.forEach((path, content) -> { + ProjectFile file = new ProjectFile(path, content); + if (file.name().equals(matchName)) { + result.add(file); + } + }); return result; } + + private boolean isConfigFile(String path) { + if (path == null || path.isBlank()) { + return false; + } + String normalized = normalizeConfigPath(path).toLowerCase(Locale.ROOT); + return normalized.equals("tsconfig.json") + || normalized.endsWith("/tsconfig.json") + || normalized.matches(".*/tsconfig\\.[^/]+\\.json$") + || normalized.equals("jsconfig.json") + || normalized.endsWith("/jsconfig.json") + || normalized.equals("package.json") + || normalized.endsWith("/package.json") + || normalized.equals("pyrightconfig.json") + || normalized.endsWith("/pyrightconfig.json") + || normalized.equals("pyproject.toml") + || normalized.endsWith("/pyproject.toml"); + } + + private String normalizeConfigPath(String path) { + if (path == null || path.isBlank()) { + return path; + } + String normalized = path.replace('\\', '/'); + if (CompilerSupport.isAbsolutePath(normalized)) { + if (normalized.length() > 2 + && Character.isLetter(normalized.charAt(0)) + && normalized.charAt(1) == ':') { + normalized = normalized.substring(2); + } + } + while (normalized.startsWith("/")) { + normalized = normalized.substring(1); + } + return normalized; + } } diff --git a/src/test/java/com/hadi/test/ProjectFilesTest.java b/src/test/java/com/hadi/test/ProjectFilesTest.java index 2eeb322b..2532bb5b 100644 --- a/src/test/java/com/hadi/test/ProjectFilesTest.java +++ b/src/test/java/com/hadi/test/ProjectFilesTest.java @@ -5,6 +5,7 @@ import com.hadi.clarpse.compiler.ProjectFile; import com.hadi.clarpse.compiler.ProjectFiles; import com.hadi.clarpse.compiler.typescript.NodeRuntime; +import org.apache.commons.io.FileUtils; import org.junit.BeforeClass; import org.junit.Assume; import org.junit.Test; @@ -138,6 +139,24 @@ public void testInsertUnsupportedFileIsSkipped() { assertEquals(0, pfs.size()); } + @Test + public void testInsertConfigFileIsExposedByFiles() { + ProjectFiles pfs = new ProjectFiles(); + pfs.insertFile(new ProjectFile("/tsconfig.json", "{}")); + assertEquals(1, pfs.size()); + assertEquals(1, pfs.files().size()); + assertTrue(pfs.files().stream().anyMatch(file -> "/tsconfig.json".equals(file.path()))); + } + + @Test + public void testRemoveConfigFileRemovesItFromAllFiles() { + ProjectFiles pfs = new ProjectFiles(); + pfs.insertFile(new ProjectFile("/package.json", "{\"name\":\"demo\"}")); + assertTrue(pfs.removeFile("/package.json")); + assertEquals(0, pfs.size()); + assertEquals(0, pfs.files().size()); + } + @Test public void testEmptyProjectFilesSize() { ProjectFiles pfs = new ProjectFiles(); @@ -158,6 +177,13 @@ public void testMatchingFilesByNameNoMatch() { assertEquals(0, pfs.matchingFilesByName("missing.java").size()); } + @Test + public void testMatchingFilesByNameIncludesConfigFiles() { + ProjectFiles pfs = new ProjectFiles(); + pfs.insertFile(new ProjectFile("/tsconfig.json", "{}")); + assertEquals(1, pfs.matchingFilesByName("tsconfig.json").size()); + } + @Test public void testShiftSubDirsTwice() { ProjectFiles projectFiles = new ProjectFiles(); @@ -216,6 +242,24 @@ public void testTsconfigPersistedFromZip() throws Exception { Files.readString(tsconfig, StandardCharsets.UTF_8)); } + @Test + public void testConfigFilesFromDirAreExposed() throws Exception { + Path tempDir = Files.createTempDirectory("clarpse-project-files"); + try { + Files.writeString(tempDir.resolve("tsconfig.json"), "{\"compilerOptions\":{}}", StandardCharsets.UTF_8); + Files.createDirectories(tempDir.resolve("src")); + Files.writeString(tempDir.resolve("src").resolve("app.ts"), "export const app = 1;", StandardCharsets.UTF_8); + + ProjectFiles projectFiles = new ProjectFiles(tempDir.toString()); + + assertEquals(2, projectFiles.size()); + assertTrue(projectFiles.files().stream().anyMatch(file -> "tsconfig.json".equals(file.name()))); + assertTrue(projectFiles.files(Lang.TYPESCRIPT).stream().anyMatch(file -> "app.ts".equals(file.name()))); + } finally { + FileUtils.deleteQuietly(tempDir.toFile()); + } + } + @Test public void testPyrightConfigPersistedFromZip() throws Exception { Path zipPath = Files.createTempFile("pyrightconfig", ".zip"); From 02f6cc7146b3c4c38413c0b6f793becfc7f8cdde Mon Sep 17 00:00:00 2001 From: zir0 Date: Sun, 19 Apr 2026 01:36:50 +0300 Subject: [PATCH 2/2] Fix ProjectFiles config loading edge cases --- .../hadi/clarpse/compiler/ProjectFiles.java | 17 +++++++++------ .../java/com/hadi/test/ProjectFilesTest.java | 21 +++++++++++++++++++ 2 files changed, 32 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/hadi/clarpse/compiler/ProjectFiles.java b/src/main/java/com/hadi/clarpse/compiler/ProjectFiles.java index 3ba5e93b..0d53675c 100644 --- a/src/main/java/com/hadi/clarpse/compiler/ProjectFiles.java +++ b/src/main/java/com/hadi/clarpse/compiler/ProjectFiles.java @@ -178,7 +178,7 @@ private void initFilesFromDir(File projectFiles) throws IOException { Iterator it = FileUtils.iterateFiles(projectFiles, null, true); while (it.hasNext()) { File nextFile = it.next(); - if (nextFile.isFile()) { + if (nextFile.isFile() && shouldLoadPath(nextFile.getAbsolutePath())) { String content = FileUtils.readFileToString(nextFile, StandardCharsets.UTF_8); this.insertFile(new ProjectFile(nextFile.getAbsolutePath(), content)); } @@ -279,11 +279,8 @@ private byte[] readEntryBytes(InputStream inputStream, long maxBytes, String ent private void handlePotentialConfigFile(String safeName, String content) { String normalizedSafeName = safeName.replace("\\", "/"); - String lower = normalizedSafeName.toLowerCase(Locale.ROOT); - if (lower.endsWith("tsconfig.json") - || lower.endsWith("pyrightconfig.json") - || lower.endsWith("pyproject.toml")) { - this.configFiles.put(normalizedSafeName, content); + if (isConfigFile(normalizedSafeName)) { + insertConfigFile(new ProjectFile(normalizedSafeName, content)); } } @@ -501,6 +498,14 @@ private boolean isConfigFile(String path) { || normalized.endsWith("/pyproject.toml"); } + private boolean shouldLoadPath(String path) { + if (path == null || path.isBlank()) { + return false; + } + return isConfigFile(path) + || Lang.langFromExtn(FilenameUtils.getExtension(path)) != null; + } + private String normalizeConfigPath(String path) { if (path == null || path.isBlank()) { return path; diff --git a/src/test/java/com/hadi/test/ProjectFilesTest.java b/src/test/java/com/hadi/test/ProjectFilesTest.java index 2532bb5b..103309ce 100644 --- a/src/test/java/com/hadi/test/ProjectFilesTest.java +++ b/src/test/java/com/hadi/test/ProjectFilesTest.java @@ -242,6 +242,27 @@ public void testTsconfigPersistedFromZip() throws Exception { Files.readString(tsconfig, StandardCharsets.UTF_8)); } + @Test + public void testZipLoadedConfigFileCountsTowardSizeAndCanBeRemoved() throws Exception { + Path zipPath = Files.createTempFile("zip-config-only", ".zip"); + try (ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(zipPath.toFile()))) { + ZipEntry configEntry = new ZipEntry("project/tsconfig.json"); + zos.putNextEntry(configEntry); + zos.write("{\"compilerOptions\":{}}".getBytes(StandardCharsets.UTF_8)); + zos.closeEntry(); + } + + ProjectFiles projectFiles; + try (var in = Files.newInputStream(zipPath)) { + projectFiles = new ProjectFiles(in); + } + + assertEquals(1, projectFiles.size()); + assertTrue(projectFiles.removeFile("/project/tsconfig.json")); + assertEquals(0, projectFiles.size()); + assertEquals(0, projectFiles.files().size()); + } + @Test public void testConfigFilesFromDirAreExposed() throws Exception { Path tempDir = Files.createTempDirectory("clarpse-project-files");