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..0d53675c 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() && shouldLoadPath(nextFile.getAbsolutePath())) {
+ String content = FileUtils.readFileToString(nextFile, StandardCharsets.UTF_8);
+ this.insertFile(new ProjectFile(nextFile.getAbsolutePath(), content));
}
}
LOGGER.info("Read " + this.size + " files.");
@@ -281,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));
}
}
@@ -309,6 +304,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 +322,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 +345,15 @@ public boolean removeFile(final String path) {
if (removedCount > 0) {
this.size -= removedCount;
LOGGER.debug("Removed " + removedCount + " file(s) at path: " + path + ".");
+ return true;
+ }
+ String normalizedConfigPath = normalizeConfigPath(path);
+ if (this.configFiles.remove(normalizedConfigPath) != null) {
+ this.size -= 1;
+ LOGGER.debug("Removed config file at path: " + normalizedConfigPath + ".");
+ return true;
}
- return removedCount > 0;
+ return false;
}
/**
@@ -366,6 +376,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 +471,56 @@ 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 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;
+ }
+ 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..103309ce 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,45 @@ 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");
+ 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");