fix(discover): keep JVM packages named like build output dirs (#2661)

Below src/<set>/{java,kotlin,scala,groovy}/ every directory is a package,
so a built-in skip name there (target, tmp, vendor, deploy, env, dist...)
is a package name, not build output. Discovery and the inventory filter
now keep such directories; the root itself, its siblings and the safety
core (.git, worktrees) stay skipped.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Leonardo trindade miranda <tmonestudio@gmail.com>
This commit is contained in:
Leonardo trindade mirandaandClaude Opus 5.5 authored and mbrl committed 2026-10-21 22:56:53 -03:00
1 parent 10ab613de3
commit 259db16da7
4 files changed
+113 -3

No files matched your search

+54 -2
View File
@@ -346,6 +346,56 @@ static bool resolve_global_excludes_path(char *out, size_t out_sz) {
return false;
}
/* JVM source-set language roots: src/<set>/<lang>/ in Maven and Gradle layouts. */
static const char *JVM_SOURCE_LANG_DIRS[] = {"java", "kotlin", "scala", "groovy", NULL};
static bool component_equals(const char *start, size_t len, const char *word) {
return strlen(word) == len && strncmp(start, word, len) == 0;
}
/* True when rel_path names a directory strictly below a JVM source root
* ("src/main/java/com/acme/target", "app/src/test/kotlin/x/tmp"). There every
* directory is a package, so a built-in skip name such as "target", "tmp" or
* "vendor" is an ordinary package name, not build output (#2661). The root
* itself and directories beside it ("src/main/target") are not packages. */
bool cbm_discover_path_in_jvm_package(const char *rel_path) {
if (!rel_path) {
return false;
}
enum { JVM_MAX_COMPONENTS = 256 };
const char *starts[JVM_MAX_COMPONENTS];
size_t lens[JVM_MAX_COMPONENTS];
size_t count = 0;
const char *p = rel_path;
while (*p && count < JVM_MAX_COMPONENTS) {
while (*p == '/' || *p == '\\') {
p++;
}
if (!*p) {
break;
}
const char *start = p;
while (*p && *p != '/' && *p != '\\') {
p++;
}
starts[count] = start;
lens[count] = (size_t)(p - start);
count++;
}
/* The language root sits at i + 2; the directory must be below it. */
for (size_t i = 0; i + 3 < count; i++) {
if (!component_equals(starts[i], lens[i], "src")) {
continue;
}
for (int l = 0; JVM_SOURCE_LANG_DIRS[l]; l++) {
if (component_equals(starts[i + 2], lens[i + 2], JVM_SOURCE_LANG_DIRS[l])) {
return true;
}
}
}
return false;
}
/* True when rel_path equals a suffix rule or ends with "/" + the rule. */
bool cbm_discover_path_has_skip_suffix(const char *rel_path) {
if (!rel_path) {
@@ -803,8 +853,10 @@ static dir_verdict_t directory_verdict(const char *entry_name, const char *rel_p
const gitignore_link_t *ignore_chain,
const cbm_gitignore_t *global_gi,
const cbm_gitignore_t *cbmignore) {
if (cbm_should_skip_dir(entry_name, opts ? opts->mode : CBM_MODE_FULL) ||
cbm_discover_path_has_skip_suffix(rel_path)) {
bool builtin_skip = cbm_should_skip_dir(entry_name, opts ? opts->mode : CBM_MODE_FULL) &&
(cbm_discover_is_safety_core_directory(entry_name) ||
!cbm_discover_path_in_jvm_package(rel_path));
if (builtin_skip || cbm_discover_path_has_skip_suffix(rel_path)) {
/* #500: a .cbmignore negation (e.g. "!obj/") whose rule is the last
* match for this dir un-skips a built-in skip-list dir — except the
* non-negatable safety core. Fall through so .gitignore/global/local
+1
View File
@@ -5,5 +5,6 @@
/* Pure legacy policy predicates; callers provide live terminated strings. */
bool cbm_discover_path_has_skip_suffix(const char *relative_path);
bool cbm_discover_is_safety_core_directory(const char *name);
bool cbm_discover_path_in_jvm_package(const char *relative_path);
#endif
+3 -1
View File
@@ -102,7 +102,9 @@ static bool cif_directory_reason(cif_context *c, size_t parent, cbm_inventory_pa
if (!cif_poll(c)) {
return false;
}
bool builtin = cbm_should_skip_dir(name, CBM_MODE_FULL);
bool builtin = cbm_should_skip_dir(name, CBM_MODE_FULL) &&
(cbm_discover_is_safety_core_directory(name) ||
!cbm_discover_path_in_jvm_package((const char *)path.data));
bool suffix = !builtin && cbm_discover_path_has_skip_suffix((const char *)path.data);
bool safety =
(builtin || suffix) && c->owner->cbm_program && cbm_discover_is_safety_core_directory(name);
+55
View File
@@ -6,6 +6,7 @@
#include "test_framework.h"
#include "test_helpers.h"
#include "discover/discover.h"
#include "discover/discover_policy_internal.h"
#include "foundation/platform.h"
typedef struct {
@@ -1328,6 +1329,58 @@ TEST(discover_cbmignore_negates_always_skip_dir) {
PASS();
}
/* ── JVM packages named like build output (issue #2661) ─────────── */
/* Below src/<set>/{java,kotlin,scala,groovy}/ every directory is a package, so
* "target", "tmp", "vendor" there are package names, not build output. */
TEST(jvm_package_path_predicate_issue2661) {
ASSERT_TRUE(cbm_discover_path_in_jvm_package("src/main/java/com/acme/target"));
ASSERT_TRUE(cbm_discover_path_in_jvm_package("app/src/test/kotlin/x/tmp"));
ASSERT_TRUE(cbm_discover_path_in_jvm_package("src/main/scala/vendor"));
/* The language root itself and its siblings are not packages. */
ASSERT_FALSE(cbm_discover_path_in_jvm_package("src/main/java"));
ASSERT_FALSE(cbm_discover_path_in_jvm_package("src/main/target"));
ASSERT_FALSE(cbm_discover_path_in_jvm_package("target"));
ASSERT_FALSE(cbm_discover_path_in_jvm_package("module/target/classes"));
ASSERT_FALSE(cbm_discover_path_in_jvm_package("src/java/main/tmp"));
ASSERT_FALSE(cbm_discover_path_in_jvm_package(NULL));
PASS();
}
TEST(discover_keeps_jvm_packages_named_like_build_dirs_issue2661) {
char *base = th_mktempdir("cbm_disc_jvm_pkg");
ASSERT(base != NULL);
th_write_file(TH_PATH(base, "src/main/java/com/acme/target/Foo.java"),
"package com.acme.target;\nclass Foo {}\n");
th_write_file(TH_PATH(base, "src/test/java/com/acme/tmp/T.java"),
"package com.acme.tmp;\nclass T {}\n");
th_write_file(TH_PATH(base, "src/main/kotlin/com/acme/vendor/V.kt"),
"package com.acme.vendor\nclass V\n");
/* Real build output stays skipped. */
th_write_file(TH_PATH(base, "target/classes/Gen.java"), "class Gen {}\n");
th_write_file(TH_PATH(base, "src/main/target/Out.java"), "class Out {}\n");
/* The safety core is never un-skipped, even below a package root. */
th_write_file(TH_PATH(base, "src/main/java/com/.git/H.java"), "class H {}\n");
cbm_discover_opts_t opts = {0};
cbm_file_info_t *files = NULL;
int count = 0;
int rc = cbm_discover(base, &opts, &files, &count);
ASSERT_EQ(rc, 0);
ASSERT_TRUE(discover_has_rel_path(files, count, "src/main/java/com/acme/target/Foo.java"));
ASSERT_TRUE(discover_has_rel_path(files, count, "src/test/java/com/acme/tmp/T.java"));
ASSERT_TRUE(discover_has_rel_path(files, count, "src/main/kotlin/com/acme/vendor/V.kt"));
ASSERT_FALSE(discover_has_rel_path(files, count, "target/classes/Gen.java"));
ASSERT_FALSE(discover_has_rel_path(files, count, "src/main/target/Out.java"));
ASSERT_FALSE(discover_has_rel_path(files, count, "src/main/java/com/.git/H.java"));
ASSERT_EQ(count, 3);
cbm_discover_free(files, count);
th_cleanup(base);
PASS();
}
/* ── Laravel compiled Blade view cache (issue #1735) ─────────────── */
/* storage/framework/views/ holds Laravel's compiled Blade templates: generated
@@ -2381,6 +2434,8 @@ SUITE(discover) {
RUN_TEST(skip_venv);
RUN_TEST(skip_dist);
RUN_TEST(skip_target);
RUN_TEST(jvm_package_path_predicate_issue2661);
RUN_TEST(discover_keeps_jvm_packages_named_like_build_dirs_issue2661);
RUN_TEST(skip_vendor);
RUN_TEST(skip_vendored);
RUN_TEST(skip_terraform);