From 99d8277c0fd12efdda0bd2202059ffd52a440c6a Mon Sep 17 00:00:00 2001 From: Siddharth Srinivasan Date: Wed, 19 Nov 2025 10:32:19 +0530 Subject: [PATCH 1/3] GradleFiles project scan should adhere to global root settings Fixed GradleFiles scan for project files to honour the global scan root settings, if any. - Similar to that in projectapi SimpleFileOwnerQueryImpl. - GradleFiles needs to necessarily traverse the parent directory hierarchy to find the root project settings. - Further, the project detection needs to give precedence to maven projects also defined for the same directory, which has a similar hierarchy traversal requirement introduced in #1280. Fixed GradleFiles to find parentScript for kotlin script. - Added a unit test case for it in GradleFilesTest. Fixed SimpleFileOwnerQueryImplementation to check for scan root dirs without allowing sibling dirs with the same name prefix. - Example: if scan root = "/tmp/app", - then "/tmp/application" should not be allowed for scan; - while "/tmp/app" and "/tmp/app/src" should remain allowed. Fixed GradleFilesTest for File comparison on MacOS. - Needed due to the presence of "/private" prefix in the canonical path of unix temp folders. Signed-off-by: Siddharth Srinivasan --- .../gradle/NbGradleProjectFactory.java | 19 +- .../modules/gradle/spi/GradleFiles.java | 164 ++++++++--- .../NbGradleProjectFactoryScanRootTest.java | 255 ++++++++++++++++++ .../gradle/spi/GradleFilesScanRootTest.java | 238 ++++++++++++++++ .../modules/gradle/spi/GradleFilesTest.java | 59 ++-- .../SimpleFileOwnerQueryImplementation.java | 16 +- 6 files changed, 688 insertions(+), 63 deletions(-) create mode 100644 extide/gradle/test/unit/src/org/netbeans/modules/gradle/NbGradleProjectFactoryScanRootTest.java create mode 100644 extide/gradle/test/unit/src/org/netbeans/modules/gradle/spi/GradleFilesScanRootTest.java diff --git a/extide/gradle/src/org/netbeans/modules/gradle/NbGradleProjectFactory.java b/extide/gradle/src/org/netbeans/modules/gradle/NbGradleProjectFactory.java index 6663416b5bfa..84b98b1a2b1c 100644 --- a/extide/gradle/src/org/netbeans/modules/gradle/NbGradleProjectFactory.java +++ b/extide/gradle/src/org/netbeans/modules/gradle/NbGradleProjectFactory.java @@ -43,9 +43,6 @@ public final class NbGradleProjectFactory implements ProjectFactory2 { @Override public ProjectManager.Result isProject2(FileObject dir) { - if (!isProject(dir)) { - return null; - } // project display name can be only safely determined if the project is loaded return isProject(dir) ? new ProjectManager.Result( null, NbGradleProject.GRADLE_PROJECT_TYPE, NbGradleProject.getIcon()) : null; @@ -57,23 +54,23 @@ public boolean isProject(FileObject dir) { } static boolean isProjectCheck(FileObject dir, final boolean preferMaven) { - if (dir == null || FileUtil.toFile(dir) == null) { + File suspect = dir == null ? null : FileUtil.toFile(dir); + if (suspect == null) { return false; } - FileObject pom = dir.getFileObject("pom.xml"); //NOI18N - if (pom != null && pom.isData()) { + File pom = GradleFiles.Searcher.searchPath(suspect, "pom.xml"); //NOI18N + if (pom != null && pom.isFile()) { if (preferMaven) { return false; } - final FileObject parent = dir.getParent(); - if (parent != null && parent.getFileObject("pom.xml") != null) { // NOI18N - return isProjectCheck(parent, preferMaven); + FileObject parent = dir.getParent(); + if (parent != null && GradleFiles.Searcher.searchPath(FileUtil.toFile(parent), "pom.xml") != null) { // NOI18N + return isProjectCheck(parent, false); } } - File suspect = FileUtil.toFile(dir); GradleFiles files = new GradleFiles(suspect); if (files.isRootProject() || files.isBuildSrcProject()) return true; - + if ((files.getSettingsScript() != null) && !files.isBuildSrcProject()) { SubProjectDiskCache spCache = SubProjectDiskCache.get(files.getRootDir()); SubProjectDiskCache.SubProjectInfo data = spCache.loadData(); diff --git a/extide/gradle/src/org/netbeans/modules/gradle/spi/GradleFiles.java b/extide/gradle/src/org/netbeans/modules/gradle/spi/GradleFiles.java index eca56b7bdb45..2626208c8eda 100644 --- a/extide/gradle/src/org/netbeans/modules/gradle/spi/GradleFiles.java +++ b/extide/gradle/src/org/netbeans/modules/gradle/spi/GradleFiles.java @@ -30,11 +30,13 @@ import java.util.HashMap; import java.util.HashSet; import java.util.ArrayList; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.WeakHashMap; +import java.util.logging.Level; import java.util.logging.Logger; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -92,7 +94,7 @@ public GradleFiles(File dir) { } public GradleFiles(File dir, boolean knownProject) { - LOG.fine("Gradle Files for: " + dir.getAbsolutePath()); + LOG.log(Level.FINE, "Gradle Files for: {0}", dir.getAbsolutePath()); this.knownProject = knownProject; try { dir = dir.getCanonicalFile(); @@ -117,26 +119,14 @@ private List searchPropertyFiles() { } private void searchBuildScripts() { - File f1 = new File(projectDir, BUILD_FILE_NAME_KTS); - if (!f1.canRead()) { - f1 = new File(projectDir, BUILD_FILE_NAME); - } - File f2 = new File(projectDir, projectDir.getName() + ".gradle.kts"); //NOI18N - if (!f2.canRead()) { - f2 = new File(projectDir, projectDir.getName() + ".gradle"); //NOI18N - } - - settingsScript = searchPathUp(projectDir, SETTINGS_FILE_NAME_KTS); - if (settingsScript == null) { - settingsScript = searchPathUp(projectDir, SETTINGS_FILE_NAME); - } + buildScript = Searcher.searchPath(projectDir, BUILD_FILE_NAME_KTS, BUILD_FILE_NAME, projectDir.getName() + ".gradle.kts", projectDir.getName() + ".gradle"); //NOI18N + settingsScript = Searcher.searchPathUp(projectDir, SETTINGS_FILE_NAME_KTS, SETTINGS_FILE_NAME); File settingsDir = settingsScript != null ? settingsScript.getParentFile() : null; - buildScript = f1.canRead() ? f1 : f2.canRead() ? f2 : null; if (settingsDir != null) { //Guessing subprojects rootDir = settingsDir; - File rootScript = new File(settingsDir, BUILD_FILE_NAME); - if (rootScript.canRead() && !rootScript.equals(buildScript)) { + File rootScript = Searcher.searchPath(settingsDir, BUILD_FILE_NAME_KTS, BUILD_FILE_NAME); + if (rootScript != null && !rootScript.equals(buildScript)) { parentScript = rootScript; } } else { @@ -156,17 +146,6 @@ private void searchWrapper() { } } - private File searchPathUp(@NonNull File baseDir, @NonNull String name) { - File ret = null; - File dir = baseDir; - do { - File f = new File(dir, name); - ret = f.canRead() ? f : null; - dir = f.canRead() ? dir : dir.getParentFile(); - } while ((ret == null) && (dir != null)); - return ret; - } - public File getBuildScript() { return buildScript; } @@ -359,16 +338,16 @@ public static class SettingsFile { = Pattern.compile(".*['\\\"](.+)['\\\"].*\\.projectDir.*=.*['\\\"](.+)['\\\"].*"); //NOI18N private static final Map CACHE = new WeakHashMap<>(); - final Set subProjects = new HashSet<>(); + final Set subProjects; final long time; public SettingsFile(File f) { time = f.lastModified(); - parse(f); + subProjects = Collections.unmodifiableSet(parse(f)); } - private void parse(File f) { + private static Set parse(File f) { Map projectPaths = new HashMap<>(); String rootDir = f.getParentFile().getAbsolutePath(); try { @@ -398,13 +377,15 @@ private void parse(File f) { // Can't read the settings file for some reason. // It is ok for now simply return an emty list. } + Set subProjects = new HashSet<>(); File root = f.getParentFile(); for (Map.Entry entry : projectPaths.entrySet()) { subProjects.add(guessDir(entry.getKey(), root, new File(entry.getValue()))); } + return subProjects; } - File guessDir(String projectName, File rootDir, File firstGuess) { + private static File guessDir(String projectName, File rootDir, File firstGuess) { if (firstGuess.isDirectory()) { return firstGuess; } @@ -433,4 +414,123 @@ public static Set getSubProjects(File f) { } } + /** + * Gradle sub-project directories may not be easily identifiable and require + * scanning up the directory hierarchy up to the filesystem root for the + * settings file. + * + * Although some conventions for the {@code buildSrc} directory for shared + * modules exists, it is not generally applicable to sub-projects. + * See + * Gradle Docs: Organizing Gradle Projects + * + * This Searcher allows for safely scanning up the directory hierarchy up to + * the globally configured scan root limits ({@code -Dproject.limitScanRoot}), + * if any; + * and mindful of forbidden folders ({@code -Dproject.forbiddenFolders} + * or {@code -Dversioning.forbiddenFolders}), if any. + */ + public static class Searcher { + private static final Set forbiddenFolders; + private static final Set projectScanRoots; + + static { + Set folders = null; + Set roots = null; + try { + roots = separatePaths(System.getProperty("project.limitScanRoot"), File.pathSeparator); //NOI18N + folders = separatePaths(System.getProperty("project.forbiddenFolders", System.getProperty("versioning.forbiddenFolders")), ";"); //NOI18N + } catch (Exception e) { + LOG.log(Level.INFO, e.getMessage(), e); + } + forbiddenFolders = folders == null ? Collections.emptySet() : folders; + projectScanRoots = roots; + } + + public static File searchPath(@NonNull File baseDir, String... names) { + return resolvePathWithAlternatives(false, baseDir, names); + } + + public static File searchPathUp(@NonNull File baseDir, String... names) { + return resolvePathWithAlternatives(true, baseDir, names); + } + + private static File resolvePathWithAlternatives(boolean recursive, @NonNull File baseDir, String... names) { + File dir = baseDir; + if (names.length == 0) { + return null; + } + for (String name : names) { + Objects.requireNonNull(name); + } + while (dir != null) { + String path = dir.getAbsolutePath(); + if (notWithinProjectScanRoots(path)) + break; + if (!forbiddenFolders.contains(path)) { + for (String name : names) { + File f = new File(dir, name); + if (f.canRead()) { + return f; + } + } + } + dir = recursive ? dir.getParentFile() : null; + } + return null; + } + + private static boolean notWithinProjectScanRoots(String path) { + if (projectScanRoots == null) + return false; + for (String scanRoot : projectScanRoots) { + if (path.startsWith(scanRoot) + && (path.length() == scanRoot.length() + || path.charAt(scanRoot.length()) == File.separatorChar)) + return false; + } + return true; + } + + private static Set separatePaths(String joinedPaths, String pathSeparator) { + if (joinedPaths == null || joinedPaths.isEmpty()) + return null; + + Set paths = null; + for (String split : joinedPaths.split(pathSeparator)) { + if ((split = split.trim()).isEmpty()) continue; + + // Ensure that variations in terms of ".." or "." or windows drive-letter case differences are removed. + // File.getCanonicalFile() will additionally resolve symlinks, which is not required. + File file = FileUtil.normalizeFile(new File(split)); + + // Store both File.getAbsolutePath() and File.getCanonicalPath(), + // since File paths will be compared in this class. + String path = file.getAbsolutePath(); + if (path == null || path.isEmpty()) continue; + + String canonicalPath; + try { + canonicalPath = file.getCanonicalPath(); + } catch (IOException ioe) { + canonicalPath = null; + } + // This conversion may get rid of invalid paths. + if (canonicalPath == null || canonicalPath.isEmpty()) continue; + + if (paths == null && canonicalPath.equals(path)) { + paths = Collections.singleton(path); // more performant in usage when only a single element is present. + } else { + if (paths == null) { + paths = new LinkedHashSet<>(2); + } else if (paths.size() == 1) { + paths = new LinkedHashSet<>(paths); // more performant in iteration + } + paths.add(path); + paths.add(canonicalPath); + } + } + return paths; + } + } } diff --git a/extide/gradle/test/unit/src/org/netbeans/modules/gradle/NbGradleProjectFactoryScanRootTest.java b/extide/gradle/test/unit/src/org/netbeans/modules/gradle/NbGradleProjectFactoryScanRootTest.java new file mode 100644 index 000000000000..2f5a74c1e445 --- /dev/null +++ b/extide/gradle/test/unit/src/org/netbeans/modules/gradle/NbGradleProjectFactoryScanRootTest.java @@ -0,0 +1,255 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.netbeans.modules.gradle; + +import java.io.File; +import java.io.IOException; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; +import java.util.Enumeration; +import org.openide.filesystems.FileObject; +import org.openide.filesystems.FileUtil; +import org.openide.filesystems.LocalFileSystem; + +public class NbGradleProjectFactoryScanRootTest extends NbGradleProjectFactoryTest { + + private FileObject root; + private FileObject forbiddenTestsRoot; + + public NbGradleProjectFactoryScanRootTest(String name) { + super(name); + } + + @Override + protected void setUp() throws Exception { + super.setUp(); + LocalFileSystem fs = new LocalFileSystem(); + fs.setRootDirectory(getWorkDir()); + root = fs.getRoot(); + File parentDir = getWorkDir().getParentFile(); + File forbiddenTestsDir = new File(parentDir, "forbiddenTests"); + File fd1 = new File(forbiddenTestsDir, "forbidden1"); + File fd2 = new File(new File(forbiddenTestsDir, "f2"), "forbidden2"); + System.setProperty("project.limitScanRoot", parentDir.getAbsolutePath()); + System.setProperty("project.forbiddenFolders", fd1.getAbsolutePath() + ";" + fd2.getAbsolutePath()); + fd1.mkdirs(); + fd2.mkdirs(); + fs = new LocalFileSystem(); + fs.setRootDirectory(forbiddenTestsDir); + forbiddenTestsRoot = fs.getRoot(); + } + + @Override + protected void tearDown() throws Exception { + super.tearDown(); + Enumeration files = forbiddenTestsRoot.getData(false); + while (files.hasMoreElements()) { + try { + files.nextElement().delete(); + } catch (IOException ignore) { + } + } + Enumeration folders = forbiddenTestsRoot.getFolders(false); + while (folders.hasMoreElements()) { + try { + folders.nextElement().delete(); + } catch (IOException ignore) { + } + } + } + + public void testPomAndGradleBothNotNested() throws Exception { + FileObject parentPrj = root; + FileObject prj = FileUtil.createFolder(parentPrj, "child"); + FileObject pom = FileUtil.createData(prj, "pom.xml"); + FileObject gradle = FileUtil.createData(prj, "build.gradle"); + + assertFalse("Pom wins", NbGradleProjectFactory.isProjectCheck(prj, true)); + assertTrue("Gradle wins", NbGradleProjectFactory.isProjectCheck(prj, false)); + } + + public void testPomNestedAboveRootAndGradleNotNested() throws Exception { + File rootDir = FileUtil.toFile(root); + FileObject rootParentPrj = FileUtil.toFileObject(rootDir.getParentFile().getParentFile()); + FileObject parentPom = FileUtil.createData(rootParentPrj, "pom.xml"); + FileObject prj = FileUtil.toFileObject(rootDir.getParentFile()); + FileObject pom = FileUtil.createData(prj, "pom.xml"); + FileObject gradle = FileUtil.createData(prj, "build.gradle"); + try { + assertFalse("Pom wins", NbGradleProjectFactory.isProjectCheck(prj, true)); + assertTrue("Gradle wins", NbGradleProjectFactory.isProjectCheck(prj, false)); + prj = FileUtil.toFileObject(rootDir); + gradle.delete(); + gradle = null; + FileUtil.createData(prj, "pom.xml"); + FileUtil.createData(prj, "build.gradle"); + assertFalse("Pom wins sub", NbGradleProjectFactory.isProjectCheck(prj, true)); + assertFalse("Pom wins sub nested", NbGradleProjectFactory.isProjectCheck(prj, false)); + } finally { + try { + parentPom.delete(); + } finally { + try { + pom.delete(); + } finally { + if (gradle != null) gradle.delete(); + } + } + } + } + + public void testGetSiblingScanRoot() throws IOException { + File rootDir = FileUtil.toFile(root); + File scanRootDir = rootDir.getParentFile(); + FileObject aboveScanRoot = FileUtil.toFileObject(scanRootDir.getParentFile()); + FileObject project = aboveScanRoot.createFolder(scanRootDir.getName() + "2"); + try { + project.createData("build.gradle.kts"); + assertFalse("No gradle project scanned", NbGradleProjectFactory.isProjectCheck(project, false)); + assertFalse("No gradle project scanned with no pom", NbGradleProjectFactory.isProjectCheck(project, true)); + project.createData("pom.xml"); + assertFalse("No gradle project scanned with pom", NbGradleProjectFactory.isProjectCheck(project, false)); + assertFalse("No pom scanned", NbGradleProjectFactory.isProjectCheck(project, true)); + } finally { + project.delete(); + } + } + + /** + * Checks that project scanning does not go above root + */ + public void testAboveRootProject() throws Exception { + FileObject parentPrj = root; + parentPrj.createData("build.gradle"); + File rootDir = FileUtil.toFile(root); + String dirName = rootDir.getParentFile().getName() + '/' + rootDir.getName(); + File rootParentDir = rootDir.getParentFile().getParentFile(); + FileObject rootParentPrj = FileUtil.toFileObject(rootParentDir); + FileObject settings = FileUtil.createData(rootParentPrj, "settings.gradle"); + File likeRootDir = new File(rootParentDir, rootDir.getParentFile().getName() + "2"); + likeRootDir.mkdirs(); + File likeRootDirBuild = new File(likeRootDir, "build.gradle"); + likeRootDirBuild.createNewFile(); + try { + try (OutputStream os = settings.getOutputStream()) { + os.write(("\n" + + "rootProject.name = 'example'\n" + + "include('" + dirName + "/app')\n").getBytes(StandardCharsets.UTF_8)); + } + FileObject app = FileUtil.createFolder(parentPrj, "app"); + assertFalse("app not project", NbGradleProjectFactory.isProjectCheck(app, false)); + assertFalse("above root is not project", NbGradleProjectFactory.isProjectCheck(rootParentPrj, false)); + assertFalse("likeRoot is also not project", NbGradleProjectFactory.isProjectCheck(FileUtil.toFileObject(likeRootDir), false)); + assertTrue("root is project", NbGradleProjectFactory.isProjectCheck(parentPrj, false)); + } finally { + try { + settings.delete(); + } finally { + likeRootDirBuild.delete(); + likeRootDir.delete(); + } + } + } + + public void testForbiddenPomAndGradle() throws Exception { + FileObject parentPrj = forbiddenTestsRoot; + FileObject settings = FileUtil.createData(parentPrj, "settings.gradle"); + try (OutputStream os = settings.getOutputStream()) { + os.write(("\n" + + "rootProject.name = 'example'\n" + + "include('forbidden1')\n" + + "include('forbidden2')\n").getBytes(StandardCharsets.UTF_8)); + } + FileObject fo1 = FileUtil.createFolder(parentPrj, "forbidden1"); + FileObject fo2 = FileUtil.createFolder(parentPrj, "forbidden2"); + FileObject fo3 = FileUtil.createFolder(parentPrj, "forbidden3"); + FileUtil.createData(fo1, "pom.xml"); + FileUtil.createData(fo2, "pom.xml"); + FileUtil.createData(fo3, "pom.xml"); + assertTrue("root is project without pom", NbGradleProjectFactory.isProjectCheck(parentPrj, true)); + assertTrue("root is project", NbGradleProjectFactory.isProjectCheck(parentPrj, false)); + + assertTrue("forbidden1 is project with pom", NbGradleProjectFactory.isProjectCheck(fo1, true)); + assertFalse("forbidden2 is not project with pom", NbGradleProjectFactory.isProjectCheck(fo2, true)); + assertFalse("forbidden3 is not project with pom", NbGradleProjectFactory.isProjectCheck(fo3, true)); + assertTrue("forbidden1 is project", NbGradleProjectFactory.isProjectCheck(fo1, false)); + assertTrue("forbidden2 is project", NbGradleProjectFactory.isProjectCheck(fo2, false)); + assertFalse("forbidden3 is not project", NbGradleProjectFactory.isProjectCheck(fo3, false)); + } + + public void testForbiddenNestedPomAndGradle() throws Exception { + FileObject parentPrj = forbiddenTestsRoot.getFileObject("f2"); + FileObject settings = FileUtil.createData(parentPrj, "settings.gradle"); + try (OutputStream os = settings.getOutputStream()) { + os.write(("\n" + + "rootProject.name = 'example'\n" + + "include('forbidden1')\n" + + "include('forbidden2')\n").getBytes(StandardCharsets.UTF_8)); + } + FileObject fo1 = FileUtil.createFolder(parentPrj, "forbidden1"); + FileObject fo2 = FileUtil.createFolder(parentPrj, "forbidden2"); + FileUtil.createData(fo1, "pom.xml"); + FileUtil.createData(fo2, "pom.xml"); + FileUtil.createData(parentPrj, "pom.xml"); + FileUtil.createData(forbiddenTestsRoot, "pom.xml"); + assertFalse("root is not project with pom", NbGradleProjectFactory.isProjectCheck(parentPrj, true)); + assertFalse("root is not project with parent pom", NbGradleProjectFactory.isProjectCheck(parentPrj, false)); + + assertFalse("forbidden1 is not project with pom", NbGradleProjectFactory.isProjectCheck(fo1, true)); + assertTrue("forbidden2 is project even with parent pom and prefer maven", NbGradleProjectFactory.isProjectCheck(fo2, true)); + assertFalse("forbidden1 is not project with parent pom", NbGradleProjectFactory.isProjectCheck(fo1, false)); + assertTrue("forbidden2 is project even with parent pom without prefer maven", NbGradleProjectFactory.isProjectCheck(fo2, false)); + } + + public void testForbiddenSubProject() throws Exception { + FileObject parentPrj = forbiddenTestsRoot; + FileObject settings = FileUtil.createData(parentPrj, "settings.gradle"); + try (OutputStream os = settings.getOutputStream()) { + os.write(("\n" + + "rootProject.name = 'example'\n" + + "include('forbidden1')\n" + + "include('forbidden2')\n").getBytes(StandardCharsets.UTF_8)); + } + FileObject fo1 = FileUtil.createFolder(parentPrj, "forbidden1"); + FileObject fo2 = FileUtil.createFolder(parentPrj, "forbidden2"); + FileObject fo3 = FileUtil.createFolder(parentPrj, "forbidden3"); + FileObject f2fo2 = FileUtil.createFolder(parentPrj, "f2/forbidden2"); + FileObject f2fo2app = FileUtil.createFolder(parentPrj, "f2/forbidden2/app"); + FileObject f2f2A = FileUtil.createFolder(parentPrj, "f2/forbidden2App"); + FileUtil.createData(f2fo2, "build.gradle"); + FileObject f2fo2Settings = FileUtil.createData(f2fo2, "settings.gradle"); + try (OutputStream os = f2fo2Settings.getOutputStream()) { + os.write(("\n" + + "rootProject.name = 'eg'\n" + + "include('app')\n").getBytes(StandardCharsets.UTF_8)); + } + + assertTrue("root is project", NbGradleProjectFactory.isProjectCheck(parentPrj, false)); + assertTrue("forbidden1 is project", NbGradleProjectFactory.isProjectCheck(fo1, false)); + assertTrue("forbidden2 is project", NbGradleProjectFactory.isProjectCheck(fo2, false)); + assertFalse("forbidden3 is not project", NbGradleProjectFactory.isProjectCheck(fo3, false)); + assertFalse("f2/forbidden2App is not project", NbGradleProjectFactory.isProjectCheck(f2f2A, false)); + FileUtil.createData(f2f2A, "build.gradle"); + assertTrue("f2/forbidden2App is project with build", NbGradleProjectFactory.isProjectCheck(f2f2A, false)); + assertFalse("f2/forbidden2 is not project", NbGradleProjectFactory.isProjectCheck(f2fo2, false)); + assertFalse("f2/forbidden2/app is not project", NbGradleProjectFactory.isProjectCheck(f2fo2app, false)); + FileUtil.createData(f2fo2app, "build.gradle"); + assertTrue("f2/forbidden2/app is project with build", NbGradleProjectFactory.isProjectCheck(f2fo2app, false)); + } +} diff --git a/extide/gradle/test/unit/src/org/netbeans/modules/gradle/spi/GradleFilesScanRootTest.java b/extide/gradle/test/unit/src/org/netbeans/modules/gradle/spi/GradleFilesScanRootTest.java new file mode 100644 index 000000000000..59bafc61356a --- /dev/null +++ b/extide/gradle/test/unit/src/org/netbeans/modules/gradle/spi/GradleFilesScanRootTest.java @@ -0,0 +1,238 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.netbeans.modules.gradle.spi; + +import java.io.File; +import java.io.IOException; +import java.nio.file.FileVisitResult; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.List; +import org.junit.Test; +import org.junit.After; +import org.junit.Before; +import org.openide.util.Utilities; + +import static org.junit.Assert.*; + +public class GradleFilesScanRootTest extends GradleFilesTest { + + private File scanRoot; + private File forbiddenTestsDir; + + @Before + public void setup() { + scanRoot = root.getRoot().getParentFile(); + forbiddenTestsDir = new File(scanRoot, "forbiddenTests"); + File fd1 = new File(forbiddenTestsDir, "forbidden1"); + File fd2 = new File(new File(forbiddenTestsDir, "f2"), "forbidden2"); + System.setProperty("project.limitScanRoot", scanRoot.getAbsolutePath()); + System.setProperty("project.forbiddenFolders", fd1.getAbsolutePath() + ";" + fd2.getAbsolutePath()); + fd1.mkdirs(); + fd2.mkdirs(); + } + + @After + public void cleanup() { + try { + Files.walkFileTree(forbiddenTestsDir.toPath(), new SimpleFileVisitor() { + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { + return file.toFile().delete() ? FileVisitResult.CONTINUE : FileVisitResult.TERMINATE; + } + + @Override + public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException { + return dir.toFile().delete() ? FileVisitResult.CONTINUE : FileVisitResult.TERMINATE; + } + }); + } catch (IOException e) { + e.printStackTrace(System.err); + } + } + + private static File normalizeTempDir(File root) { + if (root != null && Utilities.isMac()) { + String absolutePath = root.getAbsolutePath(); + if (absolutePath.startsWith("/private/")) { + return new File(absolutePath.substring(8)); + } + } + return root; + } + + @Test + public void testGetProjectAboveRoot() throws IOException { + File dirAboveScanRoot = scanRoot.getParentFile(); + File settings = new File(dirAboveScanRoot, "settings.gradle"); + File build = new File(dirAboveScanRoot, "build.gradle.kts"); + try { + String subPath = root.getRoot().getAbsolutePath().substring(dirAboveScanRoot.getAbsolutePath().length() + 1); + build.createNewFile(); + settings.createNewFile(); + Files.write(settings.toPath(), List.of( + "rootProject.name = 'example'", + "include('" + subPath + "/app')" + )); + File app = root.newFolder("app"); + // Check that the project is not resolved + GradleFiles gf = new GradleFiles(app); + assertNull(gf.getBuildScript()); + assertNull(gf.getSettingsScript()); + assertNull(gf.getParentScript()); + assertFalse(gf.isProject()); + assertFalse(gf.isRootProject()); + + // Also check the root project + File rootBuild = root.newFile(root.getRoot().getName() + ".gradle.kts"); + GradleFiles rootGf = new GradleFiles(root.getRoot()); + assertEquals(normalizeTempDir(rootBuild), normalizeTempDir(rootGf.getBuildScript())); + assertNull(rootGf.getSettingsScript()); + assertNull(rootGf.getParentScript()); + assertTrue(rootGf.isProject()); + assertTrue(rootGf.isRootProject()); + + // Now ensure that the above project structure does work when below the scanRoot. + File newRoot = root.newFolder(subPath); + File newSettings = Files.move(settings.toPath(), root.getRoot().toPath().resolve(settings.getName())).toFile(); + File newBuild = Files.move(build.toPath(), root.getRoot().toPath().resolve(build.getName())).toFile(); + File newApp = Files.move(app.toPath(), newRoot.toPath().resolve(app.getName())).toFile(); + GradleFiles newGf = new GradleFiles(newApp); + assertNull(newGf.getBuildScript()); + assertEquals(normalizeTempDir(newSettings), normalizeTempDir(newGf.getSettingsScript())); + assertEquals(normalizeTempDir(newBuild), normalizeTempDir(newGf.getParentScript())); + assertTrue(newGf.isProject()); + assertFalse(gf.isRootProject()); + } finally { + settings.delete(); + build.delete(); + } + } + + @Test + public void testGetSiblingScanRoot() throws IOException { + File dirAboveScanRoot = scanRoot.getParentFile(); + File project = new File(dirAboveScanRoot, scanRoot.getName() + "2"); + File settings = new File(project, "settings.gradle"); + File build = new File(project, "build.gradle.kts"); + try { + project.mkdirs(); + build.createNewFile(); + settings.createNewFile(); + GradleFiles gf = new GradleFiles(project); + assertNull(gf.getBuildScript()); + assertNull(gf.getSettingsScript()); + assertNull(gf.getParentScript()); + assertFalse(gf.isProject()); + assertFalse(gf.isRootProject()); + } finally { + settings.delete(); + build.delete(); + project.delete(); + } + } + + @Test + public void testGetForbiddenSubProject() throws IOException { + File parentPrj = forbiddenTestsDir; + File settings = new File(parentPrj, "settings.gradle"); + settings.createNewFile(); + Files.write(settings.toPath(), List.of( + "rootProject.name = 'example'", + "include('forbidden1')", + "include('forbidden2')" + )); + File fo1 = new File(parentPrj, "forbidden1"); + fo1.mkdirs(); + File fo2 = new File(parentPrj, "forbidden2"); + fo2.mkdirs(); + File f2f2A = new File(new File(parentPrj, "f2"), "forbidden2App"); + f2f2A.mkdirs(); + File f2f2ABuild = new File(f2f2A, "build.gradle"); + f2f2ABuild.createNewFile(); + File f2fo2a = new File(new File(new File(parentPrj, "f2"), "forbidden2"), "app"); + f2fo2a.mkdirs(); + File f2fo2a2 = new File(new File(new File(parentPrj, "f2"), "forbidden2"), "app2"); + f2fo2a2.mkdirs(); + File f2fo2a2Build = new File(f2fo2a2, "build.gradle"); + f2fo2a2Build.createNewFile(); + + File f2fo2Settings = new File(f2fo2a.getParentFile(), "settings.gradle"); + f2fo2Settings.createNewFile(); + Files.write(f2fo2Settings.toPath(), List.of( + "rootProject.name = 'eg'", + "include('app')", + "include('app2')" + )); + File f2fo2Build = new File(f2fo2a.getParentFile(), "build.gradle.kts"); + f2fo2Build.createNewFile(); + + + GradleFiles gf; + gf = new GradleFiles(parentPrj); + assertEquals(normalizeTempDir(settings), normalizeTempDir(gf.getSettingsScript())); + assertTrue("root is project", gf.isProject()); + assertTrue("root is rootProject", gf.isRootProject()); + + gf = new GradleFiles(fo1); + assertNull("buildScript null for forbidden1", gf.getBuildScript()); + assertEquals(normalizeTempDir(settings), normalizeTempDir(gf.getSettingsScript())); + assertTrue("forbidden1 is project", gf.isProject()); + assertFalse("forbidden1 is not rootProject", gf.isRootProject()); + assertTrue("forbidden1 is scriptless subproject", gf.isScriptlessSubProject()); + + gf = new GradleFiles(fo2); + assertNull("buildScript null for forbidden2", gf.getBuildScript()); + assertEquals(normalizeTempDir(settings), normalizeTempDir(gf.getSettingsScript())); + assertTrue("forbidden2 is project", gf.isProject()); + assertFalse("forbidden2 is not rootProject", gf.isRootProject()); + assertTrue("forbidden2 is scriptless subproject", gf.isScriptlessSubProject()); + + gf = new GradleFiles(f2f2A); + assertEquals(normalizeTempDir(f2f2ABuild), normalizeTempDir(gf.getBuildScript())); + assertEquals(normalizeTempDir(settings), normalizeTempDir(gf.getSettingsScript())); + assertTrue("f2/forbidden2App is project", gf.isProject()); + assertFalse("f2/forbidden2App is not rootProject", gf.isRootProject()); + + gf = new GradleFiles(f2fo2a); + assertNull("buildScript null for f2/forbidden2/app", gf.getBuildScript()); + assertEquals("super-parent settingsScript for f2/forbidden2/app", normalizeTempDir(settings), normalizeTempDir(gf.getSettingsScript())); + assertNull("parentScript null for f2/forbidden2/app", gf.getParentScript()); + assertFalse("f2/forbidden2/app is not project", gf.isProject()); + assertFalse("f2/forbidden2/app is not scriptless subproject", gf.isScriptlessSubProject()); + + gf = new GradleFiles(f2fo2a2); + assertEquals(normalizeTempDir(f2fo2a2Build), normalizeTempDir(gf.getBuildScript())); + assertEquals("super-parent settingsScript for f2/forbidden2/app2", normalizeTempDir(settings), normalizeTempDir(gf.getSettingsScript())); + assertNull("parentScript null for f2/forbidden2/app2", gf.getParentScript()); + assertTrue("f2/forbidden2/app2 is project", gf.isProject()); + assertFalse("f2/forbidden2/app2 is project", gf.isRootProject()); + assertFalse("f2/forbidden2/app2 is not scriptless subproject", gf.isScriptlessSubProject()); + + new File(fo1, "build.gradle").createNewFile(); + gf = new GradleFiles(fo1); + assertNull("buildScript null for forbidden1 with build also", gf.getBuildScript()); + assertEquals(normalizeTempDir(settings), normalizeTempDir(gf.getSettingsScript())); + assertTrue("forbidden1 is project", gf.isProject()); + assertFalse("forbidden1 is not rootProject", gf.isRootProject()); + assertTrue("forbidden1 is scriptless subproject", gf.isScriptlessSubProject()); + } +} diff --git a/extide/gradle/test/unit/src/org/netbeans/modules/gradle/spi/GradleFilesTest.java b/extide/gradle/test/unit/src/org/netbeans/modules/gradle/spi/GradleFilesTest.java index 5e1ff5e0ce6d..40f1c0a039a8 100644 --- a/extide/gradle/test/unit/src/org/netbeans/modules/gradle/spi/GradleFilesTest.java +++ b/extide/gradle/test/unit/src/org/netbeans/modules/gradle/spi/GradleFilesTest.java @@ -23,6 +23,8 @@ import java.io.IOException; import java.nio.file.Files; import java.util.Arrays; +import java.util.Set; +import java.util.stream.Collectors; import org.junit.Test; import static org.junit.Assert.*; import org.junit.Rule; @@ -38,6 +40,15 @@ public class GradleFilesTest { @Rule public TemporaryFolder root = new TemporaryFolder(); + private static File normalizeTempDir(File root) { + if (root != null && Utilities.isMac()) { + String absolutePath = root.getAbsolutePath(); + if (absolutePath.startsWith("/private/")) { + return new File(absolutePath.substring(8)); + } + } + return root; + } /** * Test of getBuildScript method, of class GradleFiles. @@ -46,7 +57,7 @@ public class GradleFilesTest { public void testGetBuildScript() throws IOException { File build = root.newFile("build.gradle"); GradleFiles gf = new GradleFiles(root.getRoot()); - assertEquals(build, gf.getBuildScript()); + assertEquals(normalizeTempDir(build), normalizeTempDir(gf.getBuildScript())); } @Test @@ -87,7 +98,7 @@ public void testGetBuildScript5() throws IOException { File settings = root.newFile("settings.gradle"); GradleFiles gf = new GradleFiles(settings.getParentFile()); assertEquals("It is project", true, gf.isProject()); - assertEquals("It has settings", settings, gf.getSettingsScript()); + assertEquals("It has settings", normalizeTempDir(settings), normalizeTempDir(gf.getSettingsScript())); assertNull("No build script", gf.getBuildScript()); } @@ -95,7 +106,7 @@ public void testGetBuildScript5() throws IOException { public void testGetBuildScriptKotlin() throws IOException { File build = root.newFile("build.gradle.kts"); GradleFiles gf = new GradleFiles(root.getRoot()); - assertEquals(build, gf.getBuildScript()); + assertEquals(normalizeTempDir(build), normalizeTempDir(gf.getBuildScript())); } @Test @@ -158,7 +169,17 @@ public void testGetParentScript2() throws IOException{ File module = root.newFolder("module"); Files.write(settings.toPath(), Arrays.asList("include ':module'")); GradleFiles gf = new GradleFiles(module); - assertEquals(build, gf.getParentScript()); + assertEquals(normalizeTempDir(build), normalizeTempDir(gf.getParentScript())); + } + + @Test + public void testGetParentScriptKotlin2() throws IOException{ + File build = root.newFile("build.gradle.kts"); + File settings = root.newFile("settings.gradle"); + File module = root.newFolder("module"); + Files.write(settings.toPath(), Arrays.asList("include ':module'")); + GradleFiles gf = new GradleFiles(module); + assertEquals(normalizeTempDir(build), normalizeTempDir(gf.getParentScript())); } @Test @@ -168,7 +189,7 @@ public void testGetSettingsScript() throws IOException { File module = root.newFolder("module"); Files.write(settings.toPath(), Arrays.asList("include ':module'")); GradleFiles gf = new GradleFiles(module); - assertEquals(settings, gf.getSettingsScript()); + assertEquals(normalizeTempDir(settings), normalizeTempDir(gf.getSettingsScript())); } /** @@ -181,7 +202,7 @@ public void testGetProjectDir() throws IOException { File module = root.newFolder("module"); Files.write(settings.toPath(), Arrays.asList("include ':module'")); GradleFiles gf = new GradleFiles(module); - assertEquals(module, gf.getProjectDir()); + assertEquals(normalizeTempDir(module), normalizeTempDir(gf.getProjectDir())); } @Test @@ -202,7 +223,7 @@ public void testNonExistingGetRootDir() throws IOException { root.newFile("build.gradle"); root.newFile("settings.gradle"); GradleFiles gf = new GradleFiles(new File(root.getRoot(), "module")); - assertEquals(root.getRoot(), gf.getRootDir()); + assertEquals(normalizeTempDir(root.getRoot()), normalizeTempDir(gf.getRootDir())); } @Test @@ -210,7 +231,7 @@ public void testNonExistingGetRootDir2() throws IOException { root.newFile("build.gradle"); root.newFile("settings.gradle"); GradleFiles gf = new GradleFiles(new File(root.getRoot(), "module")); - assertEquals(root.getRoot().getAbsolutePath(), gf.getRootDir().getAbsolutePath()); + assertEquals(normalizeTempDir(root.getRoot()).getAbsolutePath(), normalizeTempDir(gf.getRootDir()).getAbsolutePath()); } /** * Test of getGradlew method, of class GradleFiles. @@ -224,7 +245,7 @@ public void testGetGradlew() throws IOException { File wrapperProps = new File(root.newFolder("gradle", "wrapper"), "gradle-wrapper.properties"); wrapperProps.createNewFile(); GradleFiles gf = new GradleFiles(root.getRoot()); - assertEquals(Utilities.isWindows() ? gradlewBat : gradlew, gf.getGradlew()); + assertEquals(normalizeTempDir(Utilities.isWindows() ? gradlewBat : gradlew), normalizeTempDir(gf.getGradlew())); } /** @@ -239,7 +260,7 @@ public void testGetWrapperProperties() throws IOException { File wrapperProps = new File(root.newFolder("gradle", "wrapper"), "gradle-wrapper.properties"); wrapperProps.createNewFile(); GradleFiles gf = new GradleFiles(root.getRoot()); - assertEquals(wrapperProps, gf.getWrapperProperties()); + assertEquals(normalizeTempDir(wrapperProps), normalizeTempDir(gf.getWrapperProperties())); } @Test @@ -457,11 +478,11 @@ public void testGetFile() throws IOException { File subBuild = new File(module, "build.gradle"); subBuild.createNewFile(); GradleFiles gf = new GradleFiles(module); - assertEquals(subBuild, gf.getFile(GradleFiles.Kind.BUILD_SCRIPT)); - assertEquals(build, gf.getFile(GradleFiles.Kind.ROOT_SCRIPT)); - assertEquals(settings, gf.getFile(GradleFiles.Kind.SETTINGS_SCRIPT)); - assertEquals(buildProps, gf.getFile(GradleFiles.Kind.ROOT_PROPERTIES)); - assertEquals(new File(module, "gradle.properties"), gf.getFile(GradleFiles.Kind.PROJECT_PROPERTIES)); + assertEquals(normalizeTempDir(subBuild), normalizeTempDir(gf.getFile(GradleFiles.Kind.BUILD_SCRIPT))); + assertEquals(normalizeTempDir(build), normalizeTempDir(gf.getFile(GradleFiles.Kind.ROOT_SCRIPT))); + assertEquals(normalizeTempDir(settings), normalizeTempDir(gf.getFile(GradleFiles.Kind.SETTINGS_SCRIPT))); + assertEquals(normalizeTempDir(buildProps), normalizeTempDir(gf.getFile(GradleFiles.Kind.ROOT_PROPERTIES))); + assertEquals(normalizeTempDir(new File(module, "gradle.properties")), normalizeTempDir(gf.getFile(GradleFiles.Kind.PROJECT_PROPERTIES))); } /** @@ -472,9 +493,11 @@ public void testGetProjectFiles() throws IOException { File build = root.newFile("build.gradle"); File settings = root.newFile("settings.gradle"); GradleFiles gf = new GradleFiles(root.getRoot()); - assertEquals(2, gf.getProjectFiles().size()); - assertTrue(gf.getProjectFiles().contains(build)); - assertTrue(gf.getProjectFiles().contains(settings)); + Set projectFiles = gf.getProjectFiles(); + assertEquals(2, projectFiles.size()); + projectFiles = projectFiles.stream().map(GradleFilesTest::normalizeTempDir).collect(Collectors.toSet()); + assertTrue(projectFiles.contains(normalizeTempDir(build))); + assertTrue(projectFiles.contains(normalizeTempDir(settings))); } /** diff --git a/ide/projectapi/src/org/netbeans/modules/projectapi/SimpleFileOwnerQueryImplementation.java b/ide/projectapi/src/org/netbeans/modules/projectapi/SimpleFileOwnerQueryImplementation.java index 1068f7f87718..eac5b265257d 100644 --- a/ide/projectapi/src/org/netbeans/modules/projectapi/SimpleFileOwnerQueryImplementation.java +++ b/ide/projectapi/src/org/netbeans/modules/projectapi/SimpleFileOwnerQueryImplementation.java @@ -108,10 +108,10 @@ public void resetLastFoundReferences() { public Project getOwner(FileObject f) { List folders = new ArrayList<>(); - + deserialize(); while (f != null) { - if (projectScanRoots != null && projectScanRoots.stream().noneMatch(f.getPath()::startsWith)) { + if (notWithinProjectScanRoots(f)) { break; } boolean folder = f.isFolder(); @@ -210,6 +210,18 @@ public Project getOwner(FileObject f) { return null; } + private static boolean notWithinProjectScanRoots(FileObject f) { + if (projectScanRoots == null) + return false; + String path = f.getPath(); + for (String scanRoot : projectScanRoots) { + if (path.startsWith(scanRoot) + && (path.length() == scanRoot.length() + || path.charAt(scanRoot.length()) == '/')) + return false; + } + return true; + } private static boolean hasRoot( @NonNull final Set extRoots, From 0dcbb51448c331a49cb10f26576fe3a3f3002c74 Mon Sep 17 00:00:00 2001 From: Siddharth Srinivasan Date: Fri, 13 Mar 2026 15:12:52 +0530 Subject: [PATCH 2/3] Fixed GradleFilesScanRootTest to run successfully on linux Split the class into 2 classes: - GradleFilesWithScanRootTest now only runs the GradleFilesTest with the project limitScanRoot and forbiddenFolders properties set. - GradleFilesScanRootTest contains only the functionality of the scan-root and forbidden folders specifically. - Only a single JUnit @Test can be used here since the static initialization in @Before must be done once only. - This means the test root folder must not change between tests. - The actual gradle project is now a grandchild in the tree of the test root dir. - The original failure was due to the alternative placement of the tests temporary folder on linux which is not sufficiently nested within the OS tmp directory. Signed-off-by: Siddharth Srinivasan --- .../gradle/spi/GradleFilesScanRootTest.java | 207 +++++++++++------- .../spi/GradleFilesWithScanRootTest.java | 66 ++++++ 2 files changed, 188 insertions(+), 85 deletions(-) create mode 100644 extide/gradle/test/unit/src/org/netbeans/modules/gradle/spi/GradleFilesWithScanRootTest.java diff --git a/extide/gradle/test/unit/src/org/netbeans/modules/gradle/spi/GradleFilesScanRootTest.java b/extide/gradle/test/unit/src/org/netbeans/modules/gradle/spi/GradleFilesScanRootTest.java index 59bafc61356a..4a473f066ff8 100644 --- a/extide/gradle/test/unit/src/org/netbeans/modules/gradle/spi/GradleFilesScanRootTest.java +++ b/extide/gradle/test/unit/src/org/netbeans/modules/gradle/spi/GradleFilesScanRootTest.java @@ -27,20 +27,25 @@ import java.nio.file.attribute.BasicFileAttributes; import java.util.List; import org.junit.Test; -import org.junit.After; import org.junit.Before; import org.openide.util.Utilities; import static org.junit.Assert.*; +import org.junit.Rule; +import org.junit.rules.TemporaryFolder; -public class GradleFilesScanRootTest extends GradleFilesTest { +public class GradleFilesScanRootTest { + + @Rule + public final TemporaryFolder root = new TemporaryFolder(); private File scanRoot; private File forbiddenTestsDir; @Before public void setup() { - scanRoot = root.getRoot().getParentFile(); + scanRoot = new File(root.getRoot(), "scanRoot"); + scanRoot.mkdirs(); forbiddenTestsDir = new File(scanRoot, "forbiddenTests"); File fd1 = new File(forbiddenTestsDir, "forbidden1"); File fd2 = new File(new File(forbiddenTestsDir, "f2"), "forbidden2"); @@ -50,25 +55,6 @@ public void setup() { fd2.mkdirs(); } - @After - public void cleanup() { - try { - Files.walkFileTree(forbiddenTestsDir.toPath(), new SimpleFileVisitor() { - @Override - public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { - return file.toFile().delete() ? FileVisitResult.CONTINUE : FileVisitResult.TERMINATE; - } - - @Override - public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException { - return dir.toFile().delete() ? FileVisitResult.CONTINUE : FileVisitResult.TERMINATE; - } - }); - } catch (IOException e) { - e.printStackTrace(System.err); - } - } - private static File normalizeTempDir(File root) { if (root != null && Utilities.isMac()) { String absolutePath = root.getAbsolutePath(); @@ -80,78 +66,129 @@ private static File normalizeTempDir(File root) { } @Test - public void testGetProjectAboveRoot() throws IOException { - File dirAboveScanRoot = scanRoot.getParentFile(); + public void testAllScanRootTests() throws IOException { + // Due to the use of System.properties and the static initialization of GradleFiles, + // only a single @Test is used and individual tests are invoked from here. + SingleTestRunner runner = new SingleTestRunner(root.getRoot()); + runner.runOneTest(() -> testGetProjectAboveRoot()); + runner.runOneTest(() -> testGetSiblingScanRoot()); + runner.runOneTest(() -> testGetForbiddenSubProject()); + if (runner.getException() != null) { + throw runner.getException(); + } + } + + private interface SingleUnit { + void run() throws IOException; + } + + private static class SingleTestRunner { + private IOException exception; + private final File root; + + public SingleTestRunner(File root) { + this.root = root; + } + + public void runOneTest(SingleUnit test) { + try { + test.run(); + } catch (IOException e) { + e.printStackTrace(System.err); + if (exception == null) { + exception = e; + } else { + exception.addSuppressed(e); + } + } finally { + cleanup(); + } + } + + public IOException getException() { + return exception; + } + + private void cleanup() { + try { + Files.walkFileTree(root.toPath(), new SimpleFileVisitor() { + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { + return file.toFile().delete() ? FileVisitResult.CONTINUE : FileVisitResult.TERMINATE; + } + }); + } catch (IOException e) { + e.printStackTrace(System.err); + } + + } + } + + private void testGetProjectAboveRoot() throws IOException { + File dirAboveScanRoot = root.getRoot(); + File projectRoot = new File(scanRoot, "project"); File settings = new File(dirAboveScanRoot, "settings.gradle"); File build = new File(dirAboveScanRoot, "build.gradle.kts"); - try { - String subPath = root.getRoot().getAbsolutePath().substring(dirAboveScanRoot.getAbsolutePath().length() + 1); - build.createNewFile(); - settings.createNewFile(); - Files.write(settings.toPath(), List.of( - "rootProject.name = 'example'", - "include('" + subPath + "/app')" - )); - File app = root.newFolder("app"); - // Check that the project is not resolved - GradleFiles gf = new GradleFiles(app); - assertNull(gf.getBuildScript()); - assertNull(gf.getSettingsScript()); - assertNull(gf.getParentScript()); - assertFalse(gf.isProject()); - assertFalse(gf.isRootProject()); - - // Also check the root project - File rootBuild = root.newFile(root.getRoot().getName() + ".gradle.kts"); - GradleFiles rootGf = new GradleFiles(root.getRoot()); - assertEquals(normalizeTempDir(rootBuild), normalizeTempDir(rootGf.getBuildScript())); - assertNull(rootGf.getSettingsScript()); - assertNull(rootGf.getParentScript()); - assertTrue(rootGf.isProject()); - assertTrue(rootGf.isRootProject()); - - // Now ensure that the above project structure does work when below the scanRoot. - File newRoot = root.newFolder(subPath); - File newSettings = Files.move(settings.toPath(), root.getRoot().toPath().resolve(settings.getName())).toFile(); - File newBuild = Files.move(build.toPath(), root.getRoot().toPath().resolve(build.getName())).toFile(); - File newApp = Files.move(app.toPath(), newRoot.toPath().resolve(app.getName())).toFile(); - GradleFiles newGf = new GradleFiles(newApp); - assertNull(newGf.getBuildScript()); - assertEquals(normalizeTempDir(newSettings), normalizeTempDir(newGf.getSettingsScript())); - assertEquals(normalizeTempDir(newBuild), normalizeTempDir(newGf.getParentScript())); - assertTrue(newGf.isProject()); - assertFalse(gf.isRootProject()); - } finally { - settings.delete(); - build.delete(); - } + + projectRoot.mkdirs(); + String subPath = projectRoot.getAbsolutePath().substring(dirAboveScanRoot.getAbsolutePath().length() + 1); + build.createNewFile(); + settings.createNewFile(); + Files.write(settings.toPath(), List.of( + "projectRootProject.name = 'example'", + "include('" + subPath + "/app')" + )); + File app = new File(projectRoot, "app"); + app.mkdirs(); + // Check that the project is not resolved + GradleFiles gf = new GradleFiles(app); + assertNull(gf.getBuildScript()); + assertNull(gf.getSettingsScript()); + assertNull(gf.getParentScript()); + assertFalse(gf.isProject()); + assertFalse(gf.isRootProject()); + + // Also check the projectRoot project + File projectRootBuild = new File(projectRoot, projectRoot.getName() + ".gradle.kts"); + projectRootBuild.createNewFile(); + GradleFiles projectRootGf = new GradleFiles(projectRoot); + assertEquals(normalizeTempDir(projectRootBuild), normalizeTempDir(projectRootGf.getBuildScript())); + assertNull(projectRootGf.getSettingsScript()); + assertNull(projectRootGf.getParentScript()); + assertTrue(projectRootGf.isProject()); + assertTrue(projectRootGf.isRootProject()); + + // Now ensure that the above project structure does work when below the scanRoot. + File newRoot = new File(projectRoot, subPath); + newRoot.mkdirs(); + File newSettings = Files.move(settings.toPath(), projectRoot.toPath().resolve(settings.getName())).toFile(); + File newBuild = Files.move(build.toPath(), projectRoot.toPath().resolve(build.getName())).toFile(); + File newApp = Files.move(app.toPath(), newRoot.toPath().resolve(app.getName())).toFile(); + GradleFiles newGf = new GradleFiles(newApp); + assertNull(newGf.getBuildScript()); + assertEquals(normalizeTempDir(newSettings), normalizeTempDir(newGf.getSettingsScript())); + assertEquals(normalizeTempDir(newBuild), normalizeTempDir(newGf.getParentScript())); + assertTrue(newGf.isProject()); + assertFalse(gf.isRootProject()); } - @Test - public void testGetSiblingScanRoot() throws IOException { - File dirAboveScanRoot = scanRoot.getParentFile(); + private void testGetSiblingScanRoot() throws IOException { + File dirAboveScanRoot = root.getRoot(); File project = new File(dirAboveScanRoot, scanRoot.getName() + "2"); File settings = new File(project, "settings.gradle"); File build = new File(project, "build.gradle.kts"); - try { - project.mkdirs(); - build.createNewFile(); - settings.createNewFile(); - GradleFiles gf = new GradleFiles(project); - assertNull(gf.getBuildScript()); - assertNull(gf.getSettingsScript()); - assertNull(gf.getParentScript()); - assertFalse(gf.isProject()); - assertFalse(gf.isRootProject()); - } finally { - settings.delete(); - build.delete(); - project.delete(); - } + project.mkdirs(); + build.createNewFile(); + settings.createNewFile(); + GradleFiles gf = new GradleFiles(project); + assertNull(gf.getBuildScript()); + assertNull(gf.getSettingsScript()); + assertNull(gf.getParentScript()); + assertFalse(gf.isProject()); + assertFalse(gf.isRootProject()); } - @Test - public void testGetForbiddenSubProject() throws IOException { + private void testGetForbiddenSubProject() throws IOException { File parentPrj = forbiddenTestsDir; File settings = new File(parentPrj, "settings.gradle"); settings.createNewFile(); diff --git a/extide/gradle/test/unit/src/org/netbeans/modules/gradle/spi/GradleFilesWithScanRootTest.java b/extide/gradle/test/unit/src/org/netbeans/modules/gradle/spi/GradleFilesWithScanRootTest.java new file mode 100644 index 000000000000..d7264ba571eb --- /dev/null +++ b/extide/gradle/test/unit/src/org/netbeans/modules/gradle/spi/GradleFilesWithScanRootTest.java @@ -0,0 +1,66 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.netbeans.modules.gradle.spi; + +import java.io.File; +import java.io.IOException; +import java.nio.file.FileVisitResult; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.attribute.BasicFileAttributes; +import org.junit.After; +import org.junit.Before; + +public class GradleFilesWithScanRootTest extends GradleFilesTest { + + private File scanRoot; + private File forbiddenTestsDir; + + @Before + public void setup() { + scanRoot = root.getRoot().getParentFile(); + forbiddenTestsDir = new File(scanRoot, "forbiddenTests"); + File fd1 = new File(forbiddenTestsDir, "forbidden1"); + File fd2 = new File(new File(forbiddenTestsDir, "f2"), "forbidden2"); + System.setProperty("project.limitScanRoot", scanRoot.getAbsolutePath()); + System.setProperty("project.forbiddenFolders", fd1.getAbsolutePath() + ";" + fd2.getAbsolutePath()); + fd1.mkdirs(); + fd2.mkdirs(); + } + + @After + public void cleanup() { + try { + Files.walkFileTree(forbiddenTestsDir.toPath(), new SimpleFileVisitor() { + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { + return file.toFile().delete() ? FileVisitResult.CONTINUE : FileVisitResult.TERMINATE; + } + + @Override + public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException { + return dir.toFile().delete() ? FileVisitResult.CONTINUE : FileVisitResult.TERMINATE; + } + }); + } catch (IOException e) { + e.printStackTrace(System.err); + } + } +} From af01fc0506fd6ad032e94a0efb987df770374fbe Mon Sep 17 00:00:00 2001 From: Siddharth Srinivasan Date: Thu, 14 May 2026 23:59:06 +0530 Subject: [PATCH 3/3] GradleFiles project scan should adhere to global root settings 1. Fixed the separatePaths() method in GradleFiles.Searcher and SimpleFileOwnerQueryImplementation to not trim() the split paths and only ignore completely blank paths. 2. Added implementation notes linking the logic and code of the above two classes so that changes made in the future are done to both. Signed-off-by: Siddharth Srinivasan --- .../modules/gradle/NbGradleProjectFactory.java | 2 +- .../netbeans/modules/gradle/spi/GradleFiles.java | 10 +++++++++- .../gradle/spi/GradleFilesScanRootTest.java | 5 +++++ .../modules/gradle/spi/GradleFilesTest.java | 5 +++++ .../SimpleFileOwnerQueryImplementation.java | 16 +++++++++++++++- 5 files changed, 35 insertions(+), 3 deletions(-) diff --git a/extide/gradle/src/org/netbeans/modules/gradle/NbGradleProjectFactory.java b/extide/gradle/src/org/netbeans/modules/gradle/NbGradleProjectFactory.java index 84b98b1a2b1c..fafca918e4f9 100644 --- a/extide/gradle/src/org/netbeans/modules/gradle/NbGradleProjectFactory.java +++ b/extide/gradle/src/org/netbeans/modules/gradle/NbGradleProjectFactory.java @@ -65,7 +65,7 @@ static boolean isProjectCheck(FileObject dir, final boolean preferMaven) { } FileObject parent = dir.getParent(); if (parent != null && GradleFiles.Searcher.searchPath(FileUtil.toFile(parent), "pom.xml") != null) { // NOI18N - return isProjectCheck(parent, false); + return isProjectCheck(parent, preferMaven); } } GradleFiles files = new GradleFiles(suspect); diff --git a/extide/gradle/src/org/netbeans/modules/gradle/spi/GradleFiles.java b/extide/gradle/src/org/netbeans/modules/gradle/spi/GradleFiles.java index 2626208c8eda..6dc868f9fae0 100644 --- a/extide/gradle/src/org/netbeans/modules/gradle/spi/GradleFiles.java +++ b/extide/gradle/src/org/netbeans/modules/gradle/spi/GradleFiles.java @@ -429,6 +429,14 @@ public static Set getSubProjects(File f) { * if any; * and mindful of forbidden folders ({@code -Dproject.forbiddenFolders} * or {@code -Dversioning.forbiddenFolders}), if any. + * + * @implNote + * The logic and most of the code of scanning with limits is shared with + * {@code org.netbeans.modules.projectapi.SimpleFileOwnerQueryImplementation}; + * with the replacement of handling paths from {@code FileObject} instances + * to handling paths from {@code File} instances here, instead. + * If any logical changes are made here, they should also + * be kept in sync with {@code SimpleFileOwnerQueryImplementation}. */ public static class Searcher { private static final Set forbiddenFolders; @@ -498,7 +506,7 @@ private static Set separatePaths(String joinedPaths, String pathSeparato Set paths = null; for (String split : joinedPaths.split(pathSeparator)) { - if ((split = split.trim()).isEmpty()) continue; + if (split.isBlank()) continue; // Ensure that variations in terms of ".." or "." or windows drive-letter case differences are removed. // File.getCanonicalFile() will additionally resolve symlinks, which is not required. diff --git a/extide/gradle/test/unit/src/org/netbeans/modules/gradle/spi/GradleFilesScanRootTest.java b/extide/gradle/test/unit/src/org/netbeans/modules/gradle/spi/GradleFilesScanRootTest.java index 4a473f066ff8..672f58d9f3c6 100644 --- a/extide/gradle/test/unit/src/org/netbeans/modules/gradle/spi/GradleFilesScanRootTest.java +++ b/extide/gradle/test/unit/src/org/netbeans/modules/gradle/spi/GradleFilesScanRootTest.java @@ -59,6 +59,11 @@ private static File normalizeTempDir(File root) { if (root != null && Utilities.isMac()) { String absolutePath = root.getAbsolutePath(); if (absolutePath.startsWith("/private/")) { + // On MacOS X 10.5 and later macOS versions, the unix temporary + // directory "/tmp" is a symbolic link to "/private/tmp". + // Since the test libraries, and Java itself, set the tmpdir as + // "/tmp/...", later comparisons/assertions with a canonical + // path require stripping the "/private" prefix, if present. return new File(absolutePath.substring(8)); } } diff --git a/extide/gradle/test/unit/src/org/netbeans/modules/gradle/spi/GradleFilesTest.java b/extide/gradle/test/unit/src/org/netbeans/modules/gradle/spi/GradleFilesTest.java index 40f1c0a039a8..b646770152de 100644 --- a/extide/gradle/test/unit/src/org/netbeans/modules/gradle/spi/GradleFilesTest.java +++ b/extide/gradle/test/unit/src/org/netbeans/modules/gradle/spi/GradleFilesTest.java @@ -44,6 +44,11 @@ private static File normalizeTempDir(File root) { if (root != null && Utilities.isMac()) { String absolutePath = root.getAbsolutePath(); if (absolutePath.startsWith("/private/")) { + // On MacOS X 10.5 and later macOS versions, the unix temporary + // directory "/tmp" is a symbolic link to "/private/tmp". + // Since the test libraries, and Java itself, set the tmpdir as + // "/tmp/...", later comparisons/assertions with a canonical + // path require stripping the "/private" prefix, if present. return new File(absolutePath.substring(8)); } } diff --git a/ide/projectapi/src/org/netbeans/modules/projectapi/SimpleFileOwnerQueryImplementation.java b/ide/projectapi/src/org/netbeans/modules/projectapi/SimpleFileOwnerQueryImplementation.java index eac5b265257d..3094afcb3a1f 100644 --- a/ide/projectapi/src/org/netbeans/modules/projectapi/SimpleFileOwnerQueryImplementation.java +++ b/ide/projectapi/src/org/netbeans/modules/projectapi/SimpleFileOwnerQueryImplementation.java @@ -52,7 +52,18 @@ /** * Finds a project by searching the directory tree. + * + * This allows for safely scanning up the directory hierarchy, up to the + * globally configured scan root limits ({@code -Dproject.limitScanRoot}), if + * any; and mindful of forbidden folders ({@code -Dproject.forbiddenFolders} or + * {@code -Dversioning.forbiddenFolders}), if any. + * * @author Jesse Glick + * @implNote The logic and most of the code of scanning with limits is shared to + * {@code org.netbeans.modules.gradle.spi.GradleFiles.Searcher}; with the + * replacement of handling paths from {@code FileObject} instances to handling + * paths from {@code File} instances there, instead. If any logical changes are + * made here, they should also be kept in sync in {@code GradleFiles.Searcher}. */ @org.openide.util.lookup.ServiceProvider(service=org.netbeans.spi.project.FileOwnerQueryImplementation.class, position=100) public class SimpleFileOwnerQueryImplementation implements FileOwnerQueryImplementation { @@ -219,6 +230,9 @@ private static boolean notWithinProjectScanRoots(FileObject f) { && (path.length() == scanRoot.length() || path.charAt(scanRoot.length()) == '/')) return false; + // Note: Since the path is obtained from a FileObject, + // the file-separator will always be '/', even on Windows, + // not File.separatorChar. } return true; } @@ -431,7 +445,7 @@ private static Set separatePaths(String joinedPaths, String pathSeparato Set paths = null; for (String split : joinedPaths.split(pathSeparator)) { - if ((split = split.trim()).isEmpty()) continue; + if (split.isBlank()) continue; // Ensure that variations in terms of ".." or "." or windows drive-letter case differences are removed. // File.getCanonicalFile() will additionally resolve symlinks, which is not required.