Skip to content

Commit 2de8627

Browse files
feat: add include pattern matching for test file detection
Implement traf's `changedIncludedFilesPackages` mechanism in domino. Changed files matching configurable regex patterns now directly mark their owning project as affected, regardless of whether the file is parsed by the semantic analyzer. Default pattern matches test/spec files: `\.(spec|test)\.(ts|js)x?$` This ensures that when only test files change for a project, the project is still correctly detected as affected -- matching traf's behavior. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent f230e0b commit 2de8627

4 files changed

Lines changed: 174 additions & 3 deletions

File tree

src/core.rs

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,16 @@ use crate::types::{
77
TrueAffectedConfig,
88
};
99
use crate::utils;
10+
use regex::Regex;
1011
use rustc_hash::{FxHashMap, FxHashSet};
1112
use std::collections::HashMap;
1213
use std::path::{Path, PathBuf};
1314
use std::sync::Arc;
1415
use tracing::debug;
1516

17+
/// Default include pattern: matches test/spec files (.spec.ts, .test.tsx, etc.)
18+
const DEFAULT_INCLUDE_PATTERN: &str = r"\.(spec|test)\.(ts|js)x?$";
19+
1620
/// Mutable state for tracking affected symbols during analysis
1721
struct AffectedState<'a> {
1822
affected_packages: &'a mut FxHashSet<String>,
@@ -69,6 +73,41 @@ fn find_affected_internal(
6973
let mut affected_packages = FxHashSet::default();
7074
let mut project_causes: FxHashMap<String, Vec<AffectCause>> = FxHashMap::default();
7175

76+
// Step 4b: Process included file patterns (e.g., test files)
77+
// Files matching include patterns directly mark their owning project as affected,
78+
// matching traf's changedIncludedFilesPackages behavior.
79+
let default_include = vec![DEFAULT_INCLUDE_PATTERN.to_string()];
80+
let include_patterns = if config.include.is_empty() {
81+
&default_include
82+
} else {
83+
&config.include
84+
};
85+
86+
let compiled_patterns: Vec<Regex> = include_patterns
87+
.iter()
88+
.filter_map(|p| match Regex::new(p) {
89+
Ok(re) => Some(re),
90+
Err(e) => {
91+
debug!("Invalid include pattern '{}': {}", p, e);
92+
None
93+
}
94+
})
95+
.collect();
96+
97+
for changed_file in &changed_files {
98+
let file_str = changed_file.file_path.to_string_lossy();
99+
if compiled_patterns.iter().any(|re| re.is_match(&file_str)) {
100+
if let Some(pkg) = utils::get_package_name_by_path(&changed_file.file_path, &config.projects)
101+
{
102+
debug!(
103+
"Include pattern matched {:?}, adding package '{}'",
104+
changed_file.file_path, pkg
105+
);
106+
affected_packages.insert(pkg);
107+
}
108+
}
109+
}
110+
72111
// Step 5: Partition changed files into source and non-source
73112
let (source_files, asset_files): (Vec<&ChangedFile>, Vec<&ChangedFile>) = changed_files
74113
.iter()
@@ -680,4 +719,26 @@ mod tests {
680719
assert!(affected.contains("lib1"));
681720
assert!(affected.contains("app")); // Should be added as implicit dependent
682721
}
722+
723+
#[test]
724+
fn test_default_include_pattern_matches_test_files() {
725+
let re = Regex::new(DEFAULT_INCLUDE_PATTERN).expect("Default pattern should compile");
726+
727+
// Should match spec/test files
728+
assert!(re.is_match("proj1/utils.spec.ts"));
729+
assert!(re.is_match("proj1/utils.test.ts"));
730+
assert!(re.is_match("proj1/component.spec.tsx"));
731+
assert!(re.is_match("proj1/component.test.tsx"));
732+
assert!(re.is_match("proj1/helper.spec.js"));
733+
assert!(re.is_match("proj1/helper.test.jsx"));
734+
assert!(re.is_match("tests/validation/roof.step.test.ts"));
735+
736+
// Should NOT match regular source files
737+
assert!(!re.is_match("proj1/utils.ts"));
738+
assert!(!re.is_match("proj1/component.tsx"));
739+
assert!(!re.is_match("proj1/index.js"));
740+
assert!(!re.is_match("proj1/styles.css"));
741+
assert!(!re.is_match("proj1/data.json"));
742+
assert!(!re.is_match("proj1/test-utils.ts")); // "test" in name but not .test.ts
743+
}
683744
}

src/types.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,8 +95,8 @@ pub struct TrueAffectedConfig {
9595
pub root_ts_config: Option<PathBuf>,
9696
/// Projects in the workspace
9797
pub projects: Vec<Project>,
98-
/// Additional file patterns to include
99-
#[allow(dead_code)]
98+
/// Additional file patterns to include (regex patterns).
99+
/// When empty, defaults to matching test files: `\.(spec|test)\.(ts|js)x?$`
100100
pub include: Vec<String>,
101101
/// Paths to ignore
102102
#[allow(dead_code)]

tests/fixtures/monorepo

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
Subproject commit 0d6eb7a3c5c9e4e0e19ec9a0cd80e017c55a9c59
1+
Subproject commit 10ec84a6d723c01d9fbca21e983373b552ef5bd6

tests/integration_test.rs

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1479,3 +1479,113 @@ export function anotherFn() {
14791479
"proj2 should be affected via asset → constant → export chain"
14801480
);
14811481
}
1482+
1483+
/// Helper to get affected projects with custom include patterns
1484+
fn get_affected_with_include(include: Vec<String>) -> Vec<String> {
1485+
let config = TrueAffectedConfig {
1486+
cwd: fixture_path(),
1487+
base: "main".to_string(),
1488+
root_ts_config: Some(PathBuf::from("tsconfig.json")),
1489+
projects: vec![
1490+
Project {
1491+
name: "proj1".to_string(),
1492+
source_root: PathBuf::from("proj1"),
1493+
ts_config: Some(PathBuf::from("proj1/tsconfig.json")),
1494+
implicit_dependencies: vec![],
1495+
targets: vec![],
1496+
},
1497+
Project {
1498+
name: "proj2".to_string(),
1499+
source_root: PathBuf::from("proj2"),
1500+
ts_config: Some(PathBuf::from("proj2/tsconfig.json")),
1501+
implicit_dependencies: vec![],
1502+
targets: vec![],
1503+
},
1504+
Project {
1505+
name: "proj3".to_string(),
1506+
source_root: PathBuf::from("proj3"),
1507+
ts_config: Some(PathBuf::from("proj3/tsconfig.json")),
1508+
implicit_dependencies: vec!["proj1".to_string()],
1509+
targets: vec![],
1510+
},
1511+
],
1512+
include,
1513+
ignored_paths: vec![],
1514+
};
1515+
1516+
let profiler = Arc::new(Profiler::new(false));
1517+
1518+
find_affected(config, profiler)
1519+
.expect("Failed to find affected projects")
1520+
.affected_projects
1521+
}
1522+
1523+
#[test]
1524+
fn test_included_test_file_marks_project_affected() {
1525+
let branch = TestBranch::new("test-include-spec-file");
1526+
1527+
// Create a .spec.ts file in proj1 (test file matching default include pattern)
1528+
branch.make_change(
1529+
"proj1/utils.spec.ts",
1530+
r#"import { proj1 } from './index';
1531+
1532+
describe('proj1', () => {
1533+
it('should work', () => {
1534+
expect(proj1()).toBe('proj1');
1535+
});
1536+
});
1537+
"#,
1538+
);
1539+
1540+
// Now modify only the test file
1541+
branch.make_change(
1542+
"proj1/utils.spec.ts",
1543+
r#"import { proj1 } from './index';
1544+
1545+
describe('proj1', () => {
1546+
it('should work correctly', () => {
1547+
expect(proj1()).toBe('proj1-modified');
1548+
});
1549+
});
1550+
"#,
1551+
);
1552+
1553+
// Use default include (empty vec triggers default test file pattern)
1554+
let affected = get_affected_with_include(vec![]);
1555+
1556+
// proj1 should be affected because the .spec.ts file matches the default include pattern
1557+
assert!(
1558+
affected.contains(&"proj1".to_string()),
1559+
"proj1 should be affected when a .spec.ts file changes (default include pattern). Got: {:?}",
1560+
affected
1561+
);
1562+
}
1563+
1564+
#[test]
1565+
fn test_included_file_with_custom_pattern() {
1566+
let branch = TestBranch::new("test-include-custom");
1567+
1568+
// Create a .stories.ts file in proj2
1569+
branch.make_change(
1570+
"proj2/button.stories.ts",
1571+
r#"export const Primary = { args: { label: 'Click' } };
1572+
"#,
1573+
);
1574+
1575+
// Modify the stories file
1576+
branch.make_change(
1577+
"proj2/button.stories.ts",
1578+
r#"export const Primary = { args: { label: 'Click Me' } };
1579+
"#,
1580+
);
1581+
1582+
// Use a custom include pattern that matches .stories.ts files
1583+
let affected = get_affected_with_include(vec![r"\.stories\.(ts|js)x?$".to_string()]);
1584+
1585+
// proj2 should be affected because the .stories.ts file matches the custom pattern
1586+
assert!(
1587+
affected.contains(&"proj2".to_string()),
1588+
"proj2 should be affected when a .stories.ts file changes (custom include pattern). Got: {:?}",
1589+
affected
1590+
);
1591+
}

0 commit comments

Comments
 (0)