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
56 changes: 54 additions & 2 deletions src/discover/discover.c
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions src/discover/discover_policy_internal.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
4 changes: 3 additions & 1 deletion src/discover/inventory_filter.c
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
70 changes: 70 additions & 0 deletions tests/test_discover.c
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -1328,6 +1329,73 @@ 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);

/* Moderate/fast mode: a package named like a fast-mode skip dir is kept
* below a JVM source root, while the same name at the top stays skipped. */
th_write_file(TH_PATH(base, "src/main/java/com/acme/integration/I.java"),
"package com.acme.integration;\nclass I {}\n");
th_write_file(TH_PATH(base, "integration/Top.java"), "class Top {}\n");

cbm_discover_opts_t mod_opts = {.mode = CBM_MODE_MODERATE};
files = NULL;
count = 0;
rc = cbm_discover(base, &mod_opts, &files, &count);
ASSERT_EQ(rc, 0);
ASSERT_TRUE(discover_has_rel_path(files, count, "src/main/java/com/acme/integration/I.java"));
ASSERT_FALSE(discover_has_rel_path(files, count, "integration/Top.java"));

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
Expand Down Expand Up @@ -2381,6 +2449,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);
Expand Down
18 changes: 11 additions & 7 deletions tests/test_doc_mentions_jvm.c
Original file line number Diff line number Diff line change
Expand Up @@ -578,8 +578,10 @@ TEST(doc_mentions_jvm_java_families_and_reasons) {
jt_write(&r, "src/main/java/b/Helper.java", JT_HELPER);
jt_write(&r, "src/main/java/a/Shapes.java", JT_SHAPES);
jt_write(&r, "src/main/java/a/Widget.java", JT_WIDGET);
/* a real package in a directory the indexer skips by name */
jt_write(&r, "src/main/java/a/env/Config.java", "package a.env;\npublic class Config {}\n");
/* a real package in a layout discovery still skips by name: a plain
* src/<pkg>/ tree (the open part of #2661); below src/<set>/<lang>/ the
* same package is kept since #2724 */
jt_write(&r, "src/a/env/Config.java", "package a.env;\npublic class Config {}\n");
jt_write(&r, "src/test/java/a/GadgetTests.java",
"package a;\n/** Tests {@link Gadget}. */\npublic class GadgetTests {}\n");
ASSERT_EQ(dm_index(r.repo, r.db, NULL), 0);
Expand Down Expand Up @@ -643,8 +645,8 @@ TEST(doc_mentions_jvm_java_families_and_reasons) {
ASSERT_EQ(jt_edge(db, "a.Widget", "a.GadgetTests"), 0);
/* ... while test code binds product code */
ASSERT_EQ(jt_edge(db, "a.GadgetTests", "a.Gadget"), 1);
/* not_indexed: a package of the repository in a directory discovery
* skips, named by an import and by its qualified name */
/* not_indexed: a package of the repository in a layout discovery still
* skips by name, named by an import and by its qualified name */
ASSERT_STR_EQ(jt_reason(db, w, "Config"), "not_indexed");
ASSERT_STR_EQ(jt_reason(db, w, "a.env.Other"), "not_indexed");
/* graph_gap: a package (no node), a record component, an interface
Expand Down Expand Up @@ -1117,8 +1119,10 @@ TEST(doc_mentions_jvm_kotlin_families_and_reasons) {
jt_write(&r, "src/main/kotlin/far/Helper.kt",
"package far\n\nclass Helper {\n fun run() {}\n}\n");
jt_write(&r, "src/main/kotlin/far/sub/Deep.kt", "package far.sub\n\nclass Deep\n");
/* a real package in a directory the indexer skips by name */
jt_write(&r, "src/main/kotlin/k/env/Config.kt", "package k.env\n\nclass Config\n");
/* a real package in a layout discovery still skips by name: a plain
* src/<pkg>/ tree (the open part of #2661); below src/<set>/<lang>/ the
* same package is kept since #2724 */
jt_write(&r, "src/k/env/Config.kt", "package k.env\n\nclass Config\n");
jt_write(&r, "src/test/kotlin/k/GadgetTests.kt", "package k\n\nclass GadgetTests\n");
jt_write(&r, "src/test/kotlin/k/samples/Samples.kt",
"package k.samples\n\nfun widgetSample() {}\n");
Expand Down Expand Up @@ -1170,7 +1174,7 @@ TEST(doc_mentions_jvm_kotlin_families_and_reasons) {
ASSERT_STR_EQ(jt_reason(db, w, "far.sub"), "graph_gap");
/* test_only_target */
ASSERT_STR_EQ(jt_reason(db, w, "GadgetTests"), "test_only_target");
/* not_indexed: a package of the repository in a directory discovery skips */
/* not_indexed: a package of the repository in a layout discovery still skips by name */
ASSERT_STR_EQ(jt_reason(db, w, "k.env.Config"), "not_indexed");
/* unparseable: a path with an empty name in it */
ASSERT_STR_EQ(jt_reason(db, w, "Gadget..spin"), "unparseable");
Expand Down
Loading