Skip to main content

xtask/patterns/
util.rs

1use anyhow::{Context, Result};
2use std::{fs, path::Path};
3
4pub(super) fn read_file(path: &str) -> String {
5    fs::read_to_string(path).unwrap_or_default()
6}
7
8pub(super) fn display_path(path: &Path) -> String {
9    path.strip_prefix(".")
10        .unwrap_or(path)
11        .to_string_lossy()
12        .trim_start_matches('/')
13        .to_string()
14}
15
16pub(super) fn contains_top_level_json_key(text: &str, key: &str) -> bool {
17    // Avoids serde_json in xtask. Handles both formatted JSON (key on its own
18    // line) and compact/single-line JSON (key after `{`).
19    let pattern = format!("\"{key}\"");
20    text.lines().any(|line| {
21        let content = line.trim_start().trim_start_matches('{').trim_start();
22        content.starts_with(&pattern) && content[pattern.len()..].trim_start().starts_with(':')
23    })
24}
25
26pub(super) fn size_limit(path: &Path) -> Option<usize> {
27    if path == Path::new("crates/shared/auth/src/sqlite.rs") {
28        // Vendored auth storage logic is larger than Soma surface modules;
29        // keep it visible as a warning without blocking unrelated CI gates.
30        return Some(700);
31    }
32    if path == Path::new("crates/soma/application/src/provider_registry.rs") {
33        // Provider registration and dispatch is intentionally centralized while
34        // the drop-in provider contract is settling. Keep it warning-visible.
35        return Some(400);
36    }
37    if path == Path::new("xtask/src/generated_surfaces.rs") {
38        // Generated surface docs, plugin metadata, and package catalogs are
39        // coupled by design; split once the generated contract stabilizes.
40        return Some(500);
41    }
42    if path == Path::new("xtask/src/rmcp_release_monitor.rs")
43        || path == Path::new("xtask/src/scaffold.rs")
44    {
45        // These xtask modules orchestrate cross-cutting repo automation and are
46        // being split as the automation surface settles. Keep them visible as
47        // warnings without making unrelated workflow fixes fail CI.
48        return Some(600);
49    }
50    if path
51        .to_string_lossy()
52        .starts_with("xtask/src/scripts_lane_")
53    {
54        // Transitional script ports are grouped by migration lane. Keep them
55        // visible as warnings while avoiding a hard block during the shell to
56        // xtask handoff.
57        return Some(1000);
58    }
59
60    match path.extension().and_then(|ext| ext.to_str()) {
61        Some("rs") => Some(350),
62        Some("ts" | "tsx") => Some(300),
63        _ => None,
64    }
65}
66
67pub(super) fn is_size_exempt(path: &Path) -> bool {
68    let path = path.to_string_lossy();
69    // Vendored upstream MCP schema mirrors.
70    if path.starts_with("docs/references/mcp/schema/") {
71        return true;
72    }
73    // Checked-in code generator output. `docs/PATTERNS.md` already states the
74    // policy ("must split unless generated/fixture/schema mirror"); this is
75    // the `generated` half of it. Splitting is not an option a maintainer has
76    // here - the file is rewritten wholesale by its generator on every run,
77    // and a parity test fails the build if it is hand-edited - so a size
78    // warning on one would be pure noise, never actionable.
79    //
80    // Deliberately narrow: only a `src/generated/` directory counts, not a
81    // file that merely says "generated" somewhere in its path, so this can't
82    // be used to launder a hand-written module past the limit.
83    if path.contains("/src/generated/") {
84        return true;
85    }
86    false
87}
88
89pub(super) fn is_test_file(path: &Path) -> bool {
90    let path = path.to_string_lossy();
91    path.contains("/tests/")
92        || path.ends_with("_test.rs")
93        || path.ends_with("/tests.rs")
94        || path.ends_with(".test.ts")
95        || path.ends_with(".test.tsx")
96        || path.ends_with(".spec.ts")
97        || path.ends_with(".spec.tsx")
98        || path.contains("/__tests__/")
99}
100
101pub(super) fn effective_loc(path: &Path) -> Result<usize> {
102    let text =
103        fs::read_to_string(path).with_context(|| format!("failed to read {}", path.display()))?;
104    Ok(effective_loc_from_text(
105        &text,
106        path.extension().and_then(|ext| ext.to_str()) == Some("rs"),
107    ))
108}
109
110fn effective_loc_from_text(text: &str, strip_tests: bool) -> usize {
111    let text = if strip_tests {
112        strip_inline_test_module(text)
113    } else {
114        text
115    };
116    let mut count = 0usize;
117    let mut in_block = false;
118
119    for raw in text.lines() {
120        let mut line = raw.trim();
121        if line.is_empty() {
122            continue;
123        }
124        if in_block {
125            if let Some((_, after)) = line.split_once("*/") {
126                line = after.trim();
127                in_block = false;
128                if line.is_empty() {
129                    continue;
130                }
131            } else {
132                continue;
133            }
134        }
135        if line.starts_with("//") {
136            continue;
137        }
138        if line.starts_with("/*") {
139            if let Some((_, after)) = line.split_once("*/") {
140                line = after.trim();
141                if line.is_empty() {
142                    continue;
143                }
144            } else {
145                in_block = true;
146                continue;
147            }
148        }
149        count += 1;
150    }
151    count
152}
153
154fn strip_inline_test_module(text: &str) -> &str {
155    let lines = text.lines().collect::<Vec<_>>();
156    for index in 0..lines.len().saturating_sub(1) {
157        if lines[index].trim() != "#[cfg(test)]" {
158            continue;
159        }
160        let next = lines[index + 1].trim_start();
161        if next.starts_with("mod ") || next.starts_with("pub mod ") {
162            let byte_index = lines[..index].iter().map(|line| line.len() + 1).sum();
163            return &text[..byte_index];
164        }
165    }
166    text
167}
168
169#[cfg(test)]
170mod tests {
171    use super::*;
172
173    #[test]
174    fn effective_loc_ignores_comments_blanks_and_trailing_tests() {
175        let text = r#"
176// comment
177pub fn production() {}
178
179/* block
180   comment */
181pub fn more() {}
182
183#[cfg(test)]
184mod tests {
185    fn test_only() {}
186}
187"#;
188        assert_eq!(effective_loc_from_text(text, true), 2);
189    }
190
191    #[test]
192    fn effective_loc_counts_code_after_inline_block_comment() {
193        let text = "/* license */ pub fn one() {}\nlet two = 2;";
194        assert_eq!(effective_loc_from_text(text, false), 2);
195    }
196
197    #[test]
198    fn top_level_json_key_detects_manifest_version_field() {
199        assert!(contains_top_level_json_key(
200            "{\n  \"version\": \"1\"\n}",
201            "version"
202        ));
203        assert!(!contains_top_level_json_key(
204            "{\n  \"not_version\": true\n}",
205            "version"
206        ));
207    }
208
209    #[test]
210    fn top_level_json_key_handles_compact_json() {
211        // Single-line / compact JSON: key appears after `{`
212        assert!(contains_top_level_json_key(
213            "{ \"version\": \"1\" }",
214            "version"
215        ));
216        assert!(contains_top_level_json_key(
217            "{\"version\":\"1\"}",
218            "version"
219        ));
220        // Must not match a different key
221        assert!(!contains_top_level_json_key(
222            "{ \"name\": \"foo\" }",
223            "version"
224        ));
225    }
226
227    #[test]
228    fn size_limits_skip_vendored_mcp_schema_references() {
229        assert!(is_size_exempt(Path::new(
230            "docs/references/mcp/schema/2025-11-25/schema.ts"
231        )));
232        assert!(!is_size_exempt(Path::new("apps/web/src/app/page.tsx")));
233    }
234
235    #[test]
236    fn size_limits_skip_checked_in_generator_output() {
237        assert!(is_size_exempt(Path::new(
238            "crates/shared/codex-app-server-client/clients/typescript/src/generated/openapi-types.ts"
239        )));
240        // Only a real `src/generated/` directory is exempt - a hand-written
241        // module can't opt out by working the word into its name or docs.
242        assert!(!is_size_exempt(Path::new(
243            "crates/soma/mcp/src/generated_schemas.rs"
244        )));
245        assert!(!is_size_exempt(Path::new(
246            "xtask/src/generated_surfaces.rs"
247        )));
248    }
249
250    #[test]
251    fn transitional_xtask_modules_warn_before_hard_failing() {
252        assert_eq!(
253            size_limit(Path::new("xtask/src/rmcp_release_monitor.rs")),
254            Some(600)
255        );
256        assert_eq!(size_limit(Path::new("xtask/src/scaffold.rs")), Some(600));
257    }
258}