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
6 changes: 5 additions & 1 deletion src/commands/scan.rs
Original file line number Diff line number Diff line change
Expand Up @@ -117,11 +117,15 @@ pub async fn execute_scan(
// 2b. Run index-powered deterministic scans (unused imports, dead code)
// These work even without LLM and add findings to the final report.
let root_abs = root.canonicalize().unwrap_or_else(|_| root.clone());
// Same merged exclusion set as `cora index` (#521).
let mut index_skip = config.ignore.files.clone();
index_skip.extend(config.rules_config.index_skip_files.iter().cloned());
index_skip.dedup();
let index_findings = crate::engine::index_scanner::scan_project_index(
&root_abs,
&files,
config.rules_config.max_findings,
&config.rules_config.index_skip_files,
&index_skip,
);
if !index_findings.is_empty() {
eprintln!(
Expand Down
10 changes: 7 additions & 3 deletions src/commands/watch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,13 @@ pub fn run_watch(
// Load skip patterns + brain embedding backend from config
let config =
crate::config::loader::load_config(config_path, None, None, None, None, false).ok();
let skip_patterns: Option<Vec<String>> = config
.as_ref()
.map(|c| c.rules_config.index_skip_files.clone());
// Same merged exclusion set as `cora index` (#521).
let skip_patterns: Option<Vec<String>> = config.as_ref().map(|c| {
let mut pats = c.ignore.files.clone();
pats.extend(c.rules_config.index_skip_files.iter().cloned());
pats.dedup();
pats
});

// Resolve embedding backend
let brain_mode = config
Expand Down
63 changes: 57 additions & 6 deletions src/index/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -286,14 +286,17 @@ fn load_all_fingerprints(
///
/// Returns summary stats.
pub fn index_project(conn: &Connection, root: &Path, verbose: bool) -> anyhow::Result<IndexStats> {
index_project_with_skip(conn, root, verbose, None)
index_project_with_id(conn, ensure_project(conn, root)?, root, verbose, None)
}

/// Index a project directory, with config hash invalidation.
/// Index a project directory, honoring skip patterns (glob `*`/`**`, matched
/// against paths relative to the project root).
///
/// If `skip_patterns` is provided, the config hash is compared against
/// the stored hash in the DB. If they differ, all fingerprints are
/// cleared, forcing a full re-index.
/// Patterns do two things:
/// 1. Files matching are EXCLUDED from indexing entirely (#521), so index
/// consumers like dead-code and review's index scanners never see them.
/// 2. The pattern list is hashed into the DB; a change forces a full
/// re-index so previously indexed-but-now-excluded files get purged.
pub fn index_project_with_skip(
conn: &Connection,
root: &Path,
Expand Down Expand Up @@ -344,7 +347,7 @@ pub fn index_project_with_skip(
}
}

index_project_with_id(conn, project_id, root, verbose)
index_project_with_id(conn, project_id, root, verbose, skip_patterns)
}

/// Internal: index a project with an already-resolved `project_id`.
Expand All @@ -353,6 +356,7 @@ fn index_project_with_id(
project_id: i64,
root: &Path,
verbose: bool,
skip_patterns: Option<&[String]>,
) -> anyhow::Result<IndexStats> {
let mut stats = IndexStats::default();

Expand Down Expand Up @@ -383,6 +387,16 @@ fn index_project_with_id(
continue;
}

// Config-driven exclusion (#521): honor ignore.files /
// index_skip_files so dead-code, review index scanners, and brain
// never see these files.
if skip_patterns.is_some_and(|patterns| {
crate::engine::index_scanner::should_skip_file(&rel_str, patterns)
}) {
stats.files_excluded += 1;
continue;
}

stats.files_scanned += 1;

// Compute mtime:size fingerprint — cheap, no file read needed.
Expand Down Expand Up @@ -655,6 +669,8 @@ pub struct IndexStats {
pub files_scanned: usize,
pub files_indexed: usize,
pub files_skipped: usize,
/// Files excluded by config skip patterns (ignore.files / index.skip_files).
pub files_excluded: usize,
pub symbols_indexed: usize,
pub errors: usize,
pub embedded_symbols: Option<usize>,
Expand Down Expand Up @@ -772,6 +788,41 @@ pub struct AuthService {
assert_eq!(stats.total_symbols, 0);
}

/// Regression (#521): skip patterns must EXCLUDE files from indexing
/// (previously they only invalidated fingerprints), so dead-code and
/// review's index scanners stop reporting matches from ignored dirs.
#[test]
fn test_skip_patterns_exclude_files_from_index() {
let conn = mem_conn();
let tmp = tempfile::tempdir().unwrap();
let root = tmp.path().to_path_buf();
std::fs::create_dir(root.join("examples")).unwrap();
std::fs::write(
root.join("examples").join("demo.py"),
"def client_method():\n pass\n",
)
.unwrap();
std::fs::write(root.join("core.py"), "def core_fn():\n pass\n").unwrap();

// First run: no patterns → everything indexed.
let stats = index_project_with_skip(&conn, &root, false, Some(&[])).unwrap();
assert_eq!(stats.files_scanned, 2);
assert_eq!(stats.files_excluded, 0);

// Second run WITH a pattern: examples/ excluded via full re-index…
let pats = vec!["examples/**".to_string()];
let stats = index_project_with_skip(&conn, &root, false, Some(&pats)).unwrap();
assert_eq!(stats.files_excluded, 1);
assert!(stats.files_indexed > 0, "remaining files get re-indexed");

let pid = ensure_project(&conn, &root).unwrap();
let summary = index_stats(&conn, pid).unwrap();
assert_eq!(
summary.total_files, 1,
"only core.py remains indexed after exclusion"
);
}

#[test]
fn test_reindex_replaces_symbols() {
let conn = mem_conn();
Expand Down
21 changes: 18 additions & 3 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -774,9 +774,14 @@ async fn main() -> Result<()> {
false,
)
.ok();
let skip_patterns = config
.as_ref()
.map(|c| c.rules_config.index_skip_files.clone());
// Exclusion patterns = review's ignore.files + index.skip_files
// so dead-code/index scanners respect the same ignores (#521).
let skip_patterns: Option<Vec<String>> = config.as_ref().map(|c| {
let mut pats = c.ignore.files.clone();
pats.extend(c.rules_config.index_skip_files.iter().cloned());
pats.dedup();
pats
});

// Resolve embedding backend from brain config
let brain_mode = config
Expand Down Expand Up @@ -826,6 +831,16 @@ async fn main() -> Result<()> {
.green()
);
}
if stats.files_excluded > 0 {
eprintln!(
"{}",
format!(
" {} files excluded by ignore patterns",
stats.files_excluded
)
.dimmed()
);
}
eprintln!(
"{}",
format!(
Expand Down
Loading