Skip to content

Commit cad3a32

Browse files
feat: add Swift/Xcode error and stacktrace compression
Add a new swift_cmd filter module that compresses Xcode build output and Swift crash stack traces. CompileSwift noise lines are collapsed into a count summary, build step noise is stripped, and framework frames in crash reports (UIKit, Foundation, libswiftCore, etc.) are hidden while app frames are preserved. The filter integrates into the existing error compression pipeline in error_cmd.rs and the runner post-processing chain. Closes #1.
1 parent 7fada28 commit cad3a32

File tree

6 files changed

+798
-1
lines changed

6 files changed

+798
-1
lines changed

src/error_cmd.rs

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
//! Error stacktrace compression module.
22
//!
3-
//! Detects stacktraces from 5 languages (Node.js, Python, Rust, Go, Java)
3+
//! Detects stacktraces from 6 languages (Node.js, Python, Rust, Go, Java, Swift)
44
//! and compresses them by removing framework frames, keeping only user code.
55
//! Used as a post-processor after command-specific modules run.
66
@@ -41,6 +41,14 @@ lazy_static! {
4141
// Extract function name and location from various frame formats
4242
static ref NODE_EXTRACT_RE: Regex = Regex::new(r"^\s+at\s+(?:(.+?)\s+\()?(.+):(\d+):\d+\)?").unwrap();
4343
static ref JAVA_EXTRACT_RE: Regex = Regex::new(r"^\s+at\s+([\w.$]+)\(([\w.]+):(\d+)\)").unwrap();
44+
45+
// Swift crash report detection
46+
static ref SWIFT_CRASH_FRAME_RE: Regex = Regex::new(
47+
r"^\d+\s+\S+\s+0x[0-9a-fA-F]+\s+.+"
48+
).unwrap();
49+
static ref SWIFT_CRASH_HEADER_RE: Regex = Regex::new(
50+
r"^Thread \d+( Crashed)?:"
51+
).unwrap();
4452
}
4553

4654
#[derive(Debug, PartialEq)]
@@ -50,6 +58,7 @@ enum Language {
5058
Rust,
5159
Go,
5260
Java,
61+
Swift,
5362
}
5463

5564
/// Detect the language of a stacktrace from the input text.
@@ -64,6 +73,14 @@ fn detect_language(input: &str) -> Option<Language> {
6473
if GO_GOROUTINE_RE.is_match(line) {
6574
return Some(Language::Go);
6675
}
76+
// Swift crash reports: "Thread N Crashed:" followed by dylib frames
77+
if SWIFT_CRASH_HEADER_RE.is_match(line) {
78+
// Verify there are actual Swift-style crash frames nearby
79+
let has_crash_frames = input.lines().any(|l| SWIFT_CRASH_FRAME_RE.is_match(l));
80+
if has_crash_frames {
81+
return Some(Language::Swift);
82+
}
83+
}
6784
}
6885

6986
// Node.js vs Java: both use "at " prefix. Distinguish by frame format.
@@ -98,6 +115,7 @@ pub fn compress_errors(input: &str) -> String {
98115
Some(Language::Rust) => compress_rust(&deduped),
99116
Some(Language::Go) => compress_go(&deduped),
100117
Some(Language::Java) => compress_java(&deduped),
118+
Some(Language::Swift) => crate::swift_cmd::compress_swift_crash(&deduped),
101119
None => deduped,
102120
}
103121
}

src/main.rs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ mod ruff_cmd;
5656
mod runner;
5757
mod session_cmd;
5858
mod summary;
59+
mod swift_cmd;
5960
mod tee;
6061
mod telemetry;
6162
mod toml_filter;
@@ -508,6 +509,12 @@ enum Commands {
508509
command: CargoCommands,
509510
},
510511

512+
/// Swift/Xcode commands with compact output (collapse compile lines, compress crash traces)
513+
Swift {
514+
#[command(subcommand)]
515+
command: SwiftCommands,
516+
},
517+
511518
/// npm run with filtered output (strip boilerplate)
512519
Npm {
513520
/// npm run arguments (script name + options)
@@ -978,6 +985,25 @@ enum CargoCommands {
978985
Other(Vec<OsString>),
979986
}
980987

988+
#[derive(Subcommand)]
989+
enum SwiftCommands {
990+
/// Build with compact output (collapse CompileSwift lines, keep errors)
991+
Build {
992+
/// Additional swift build arguments
993+
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
994+
args: Vec<String>,
995+
},
996+
/// Test with compact output (collapse compile noise, keep test results)
997+
Test {
998+
/// Additional swift test arguments
999+
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
1000+
args: Vec<String>,
1001+
},
1002+
/// Passthrough: runs any unsupported swift subcommand directly
1003+
#[command(external_subcommand)]
1004+
Other(Vec<OsString>),
1005+
}
1006+
9811007
#[derive(Subcommand)]
9821008
enum DotnetCommands {
9831009
/// Build with compact output
@@ -1838,6 +1864,18 @@ fn main() -> Result<()> {
18381864
}
18391865
},
18401866

1867+
Commands::Swift { command } => match command {
1868+
SwiftCommands::Build { args } => {
1869+
swift_cmd::run(swift_cmd::SwiftCommand::Build, &args, cli.verbose)?;
1870+
}
1871+
SwiftCommands::Test { args } => {
1872+
swift_cmd::run(swift_cmd::SwiftCommand::Test, &args, cli.verbose)?;
1873+
}
1874+
SwiftCommands::Other(args) => {
1875+
swift_cmd::run_passthrough(&args, cli.verbose)?;
1876+
}
1877+
},
1878+
18411879
Commands::Npm { args } => {
18421880
npm_cmd::run(&args, cli.verbose, cli.skip_env)?;
18431881
}
@@ -2241,6 +2279,7 @@ fn is_operational_command(cmd: &Commands) -> bool {
22412279
| Commands::Prettier { .. }
22422280
| Commands::Playwright { .. }
22432281
| Commands::Cargo { .. }
2282+
| Commands::Swift { .. }
22442283
| Commands::Npm { .. }
22452284
| Commands::Npx { .. }
22462285
| Commands::Curl { .. }

src/runner.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@ pub fn run_err(command: &str, verbose: u8) -> Result<()> {
5252
let compressed = crate::build_cmd::group_build_errors(&compressed);
5353
// Post-process: compress Docker build logs
5454
let compressed = crate::docker_cmd::compress_docker_log(&compressed);
55+
// Post-process: compress Xcode build logs
56+
let compressed = crate::swift_cmd::compress_xcode_log(&compressed);
5557
summary.push_str(&compressed);
5658
}
5759

0 commit comments

Comments
 (0)