diff --git a/crates/fff-core/src/grep/grep.rs b/crates/fff-core/src/grep/grep.rs index 3f53e961c..5a4af83f8 100644 --- a/crates/fff-core/src/grep/grep.rs +++ b/crates/fff-core/src/grep/grep.rs @@ -229,9 +229,19 @@ pub(crate) fn grep_search<'a>( return result; } + // Keep any explicit FilePath scope (AI mode `path/to/file.ext` prefix) so the + // fallback can't leak matches outside the file the user pinned. Only the + // swallowed operator/glob tokens are dropped. See issue #756. + let scoped_constraints: fff_query_parser::ConstraintVec<'_> = query + .constraints + .iter() + .filter(|c| matches!(c, fff_query_parser::Constraint::FilePath(_))) + .cloned() + .collect(); + let literal_query = FFFQuery { raw_query: query.raw_query, - constraints: Vec::new(), + constraints: scoped_constraints, fuzzy_query: fff_query_parser::FuzzyQuery::Text(raw), location: None, }; diff --git a/crates/fff-core/src/grep/grep_tests.rs b/crates/fff-core/src/grep/grep_tests.rs index 72222a3b0..149bde977 100644 --- a/crates/fff-core/src/grep/grep_tests.rs +++ b/crates/fff-core/src/grep/grep_tests.rs @@ -461,3 +461,58 @@ fn test_grep_no_duplicates_with_overflow_trailing_bits() { result.matches.len() ); } + +/// Issue #756: an AI-mode regex query with an inline FilePath scope and +/// top-level alternation. The regex fragments are swallowed as bogus Glob +/// constraints, the constrained search finds nothing, and the literal/regex +/// fallback must NOT drop the FilePath scope — otherwise the `|` branch leaks +/// matches into files outside the pinned path. +#[test] +fn regex_fallback_keeps_file_path_scope_issue_756() { + use fff_query_parser::{AiGrepConfig, QueryParser}; + let dir = tempfile::tempdir().unwrap(); + let base = crate::path_utils::canonicalize(dir.path()).unwrap(); + std::fs::create_dir(base.join("scope")).unwrap(); + std::fs::write( + base.join("scope").join("target.css"), + "/* ---------- target ---------- */\n", + ) + .unwrap(); + std::fs::write( + base.join("outside.css"), + "/* ---------- outside ---------- */\n", + ) + .unwrap(); + + let mut picker = FilePicker::new(FilePickerOptions { + base_path: base.to_str().unwrap().into(), + watch: false, + ..Default::default() + }) + .unwrap(); + picker.collect_files().unwrap(); + + let options = crate::GrepSearchOptions { + mode: super::GrepMode::Regex, + smart_case: true, + max_matches_per_file: 80, + page_limit: 100, + ..Default::default() + }; + + let raw = r"scope/target.css ^/\* |^\s*/\* ----------"; + let query = QueryParser::new(AiGrepConfig).parse(raw); + let result = picker.grep(&query, &options); + let mut paths: Vec = result + .files + .iter() + .map(|f| f.relative_path(&picker)) + .collect(); + paths.sort(); + + assert_eq!( + paths, + vec!["scope/target.css"], + "regex fallback must not leak outside the FilePath scope" + ); +}