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
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

<groupId>io.github.hadi-technology</groupId>
<artifactId>clarpse</artifactId>
<version>9.5.2</version>
<version>9.5.3</version>
<packaging>jar</packaging>

<name>${project.groupId}:${project.artifactId}</name>
Expand Down
83 changes: 72 additions & 11 deletions src/main/java/com/hadi/clarpse/compiler/ProjectFiles.java
Original file line number Diff line number Diff line change
Expand Up @@ -178,11 +178,9 @@ private void initFilesFromDir(File projectFiles) throws IOException {
Iterator<File> 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.");
Expand Down Expand Up @@ -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));
}
}

Expand All @@ -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() + ".");
}
Expand All @@ -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.
*
Expand All @@ -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 + ".");
Comment on lines +352 to +353

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Prevent size underflow when removing zip-loaded config files

This decrement assumes every config entry was counted in size, but zip config files are still inserted via handlePotentialConfigFile without incrementing size. After this commit exposed config files/removal paths, a zip containing only tsconfig.json can produce size()==0 initially and then removeFile(...) drives it to -1, making size() inconsistent and incorrect.

Useful? React with 👍 / 👎.

return true;
}
return removedCount > 0;
return false;
}

/**
Expand All @@ -366,6 +376,7 @@ public final Collection<ProjectFile> files(Lang language) {
public final Collection<ProjectFile> files() {
Set<ProjectFile> 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);
}

Expand Down Expand Up @@ -460,6 +471,56 @@ public Set<ProjectFile> matchingFilesByName(String matchName) {
Set<ProjectFile> 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;
}
}
65 changes: 65 additions & 0 deletions src/test/java/com/hadi/test/ProjectFilesTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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();
Expand All @@ -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();
Expand Down Expand Up @@ -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");
Expand Down
Loading