Skip to content
This repository was archived by the owner on Sep 9, 2025. It is now read-only.

Commit a8ca985

Browse files
author
Hendrik van Antwerpen
committed
Add flag to limit analysis time per file
1 parent 714af5a commit a8ca985

File tree

4 files changed

+118
-27
lines changed

4 files changed

+118
-27
lines changed

tree-sitter-stack-graphs/src/cli/analyze.rs

Lines changed: 69 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -16,31 +16,52 @@ use stack_graphs::stitching::Database;
1616
use std::collections::HashMap;
1717
use std::path::Path;
1818
use std::path::PathBuf;
19+
use std::sync::Arc;
20+
use std::time::Duration;
1921
use tree_sitter_graph::Variables;
2022
use walkdir::WalkDir;
2123

24+
use crate::cli::util::duration_from_seconds_str;
2225
use crate::cli::util::map_parse_errors;
2326
use crate::cli::util::path_exists;
2427
use crate::loader::Loader;
28+
use crate::CancelAfterDuration;
29+
use crate::CancellationFlag;
2530
use crate::LoadError;
2631
use crate::NoCancellation;
2732

2833
/// Analyze sources
2934
#[derive(Args)]
3035
pub struct AnalyzeArgs {
3136
/// Source file or directory paths.
32-
#[clap(value_name = "SOURCE_PATH", required = true, value_hint = ValueHint::AnyPath, parse(from_os_str), validator_os = path_exists)]
37+
#[clap(
38+
value_name = "SOURCE_PATH",
39+
required = true,
40+
value_hint = ValueHint::AnyPath,
41+
parse(from_os_str),
42+
validator_os = path_exists,
43+
)]
3344
pub source_paths: Vec<PathBuf>,
3445

35-
#[clap(short = 'v')]
46+
#[clap(long, short = 'v')]
3647
pub verbose: bool,
48+
49+
/// Maximum runtime per file in seconds.
50+
#[clap(
51+
long,
52+
value_name = "SECONDS",
53+
parse(try_from_str = duration_from_seconds_str),
54+
require_equals = true,
55+
)]
56+
pub max_file_time: Option<Duration>,
3757
}
3858

3959
impl AnalyzeArgs {
4060
pub fn new(source_paths: Vec<PathBuf>) -> Self {
4161
Self {
4262
source_paths,
4363
verbose: false,
64+
max_file_time: None,
4465
}
4566
}
4667

@@ -76,36 +97,41 @@ impl AnalyzeArgs {
7697
.with_context(|| format!("Error analyzing file {}", source_path.display()))
7798
}
7899

79-
/// Run test file.
80100
fn analyze_file(
81101
&self,
82102
source_root: &Path,
83103
source_path: &Path,
84104
loader: &mut Loader,
85105
) -> anyhow::Result<()> {
86106
if self.verbose {
87-
eprint!("{} ", source_path.display());
107+
eprint!("{}: ", source_path.display());
108+
}
109+
110+
let mut cancellation_flag: Arc<dyn CancellationFlag> = Arc::new(NoCancellation);
111+
if let Some(max_file_time) = self.max_file_time {
112+
cancellation_flag = CancelAfterDuration::new(max_file_time);
88113
}
89114

90115
let source = std::fs::read_to_string(source_path)?;
91-
let lc = match loader.load_for_file(source_path, Some(&source), &NoCancellation)? {
92-
Some(sgl) => sgl,
93-
None => {
94-
if self.verbose {
95-
eprintln!("{}", "⦵".dimmed());
116+
let lc = match loader.load_for_file(source_path, Some(&source), cancellation_flag.as_ref())
117+
{
118+
Ok(Some(sgl)) => sgl,
119+
Ok(None) => return Ok(()),
120+
Err(crate::loader::LoadError::Cancelled(_)) => {
121+
if !self.verbose {
122+
eprint!("{}: ", source_path.display());
96123
}
124+
eprintln!("{}", "language loading timed out".yellow());
97125
return Ok(());
98126
}
127+
Err(e) => return Err(e.into()),
99128
};
100129

101130
let mut graph = StackGraph::new();
102131
let file = graph
103132
.add_file(&source_path.to_string_lossy())
104133
.map_err(|_| anyhow!("Duplicate file {}", source_path.display()))?;
105134

106-
if self.verbose {
107-
eprint!("{} ", "🕸".yellow());
108-
}
109135
let relative_source_path = source_path.strip_prefix(source_root).unwrap();
110136
let result = if let Some(fa) = source_path
111137
.file_name()
@@ -118,45 +144,63 @@ impl AnalyzeArgs {
118144
&source,
119145
&mut std::iter::empty(),
120146
&HashMap::new(),
121-
&NoCancellation,
147+
cancellation_flag.as_ref(),
122148
)
123149
} else {
124150
let globals = Variables::new();
125-
lc.sgl
126-
.build_stack_graph_into(&mut graph, file, &source, &globals, &NoCancellation)
151+
lc.sgl.build_stack_graph_into(
152+
&mut graph,
153+
file,
154+
&source,
155+
&globals,
156+
cancellation_flag.as_ref(),
157+
)
127158
};
128159
match result {
129160
Err(LoadError::ParseErrors(parse_errors)) => {
130-
let parse_error = map_parse_errors(source_path, &parse_errors, &source);
161+
let parse_error = map_parse_errors(source_path, &parse_errors, &source, "");
131162
if !self.verbose {
132-
eprint!("{}", source_path.display());
163+
eprint!("{}: ", source_path.display());
133164
}
134-
eprintln!("{}", "".red());
165+
eprintln!("{}", "parsing failed".red());
135166
eprintln!("{}", parse_error);
136167
return Ok(());
137168
}
169+
Err(LoadError::Cancelled(_)) => {
170+
if !self.verbose {
171+
eprint!("{}: ", source_path.display());
172+
}
173+
eprintln!("{}", "parsing timed out".yellow());
174+
return Ok(());
175+
}
138176
Err(e) => return Err(e.into()),
139177
Ok(_) => {}
140178
};
141179

142-
if self.verbose {
143-
eprint!("{} ", "🦶".yellow());
144-
}
145180
let mut partials = PartialPaths::new();
146181
let mut db = Database::new();
147-
partials.find_all_partial_paths_in_file(
182+
match partials.find_all_partial_paths_in_file(
148183
&graph,
149184
file,
150-
&stack_graphs::NoCancellation,
185+
&cancellation_flag.as_ref(),
151186
|g, ps, p| {
152187
if p.is_complete_as_possible(g) {
153188
db.add_partial_path(g, ps, p);
154189
}
155190
},
156-
)?;
191+
) {
192+
Ok(_) => {}
193+
Err(_) => {
194+
if !self.verbose {
195+
eprint!("{}: ", source_path.display());
196+
}
197+
eprintln!("{}", "path computation timed out".yellow());
198+
return Ok(());
199+
}
200+
}
157201

158202
if self.verbose {
159-
eprintln!("{}", "".green());
203+
eprintln!("{}", "success".green());
160204
}
161205
Ok(())
162206
}

tree-sitter-stack-graphs/src/cli/test.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -281,7 +281,7 @@ impl TestArgs {
281281
) -> anyhow::Result<()> {
282282
match sgl.build_stack_graph_into(graph, file, source, globals, &NoCancellation) {
283283
Err(LoadError::ParseErrors(parse_errors)) => {
284-
Err(map_parse_errors(test_path, &parse_errors, source))
284+
Err(map_parse_errors(test_path, &parse_errors, source, " "))
285285
}
286286
Err(e) => Err(e.into()),
287287
Ok(_) => Ok(()),

tree-sitter-stack-graphs/src/cli/util.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ use std::ffi::OsStr;
1010
use std::ffi::OsString;
1111
use std::path::Path;
1212
use std::path::PathBuf;
13+
use std::time::Duration;
1314
use tree_sitter_graph::parse_error::TreeWithParseErrorVec;
1415

1516
use crate::cli::MAX_PARSE_ERRORS;
@@ -126,14 +127,16 @@ pub fn map_parse_errors(
126127
test_path: &Path,
127128
parse_errors: &TreeWithParseErrorVec,
128129
source: &str,
130+
prefix: &str,
129131
) -> anyhow::Error {
130132
let mut error = String::new();
131133
let parse_errors = parse_errors.errors();
132134
for parse_error in parse_errors.iter().take(MAX_PARSE_ERRORS) {
133135
let line = parse_error.node().start_position().row;
134136
let column = parse_error.node().start_position().column;
135137
error.push_str(&format!(
136-
" {}:{}:{}: {}\n",
138+
"{}{}:{}:{}: {}\n",
139+
prefix,
137140
test_path.display(),
138141
line + 1,
139142
column + 1,
@@ -150,3 +153,7 @@ pub fn map_parse_errors(
150153
}
151154
anyhow!(error)
152155
}
156+
157+
pub fn duration_from_seconds_str(s: &str) -> Result<Duration, anyhow::Error> {
158+
Ok(Duration::new(s.parse()?, 0))
159+
}

tree-sitter-stack-graphs/src/lib.rs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -322,6 +322,9 @@ use std::mem::transmute;
322322
use std::path::Path;
323323
use std::sync::atomic::AtomicUsize;
324324
use std::sync::atomic::Ordering;
325+
use std::sync::Arc;
326+
use std::time::Duration;
327+
use std::time::Instant;
325328
use thiserror::Error;
326329
use tree_sitter::Parser;
327330
use tree_sitter_graph::functions::Functions;
@@ -612,6 +615,43 @@ impl CancellationFlag for NoCancellation {
612615
}
613616
}
614617

618+
pub struct CancelAfterDuration {
619+
flag: AtomicUsize,
620+
}
621+
622+
impl CancelAfterDuration {
623+
pub fn new(limit: Duration) -> Arc<Self> {
624+
let result = Arc::new(Self {
625+
flag: AtomicUsize::new(0),
626+
});
627+
let start = Instant::now();
628+
let timer = Arc::downgrade(&result);
629+
std::thread::spawn(move || {
630+
loop {
631+
std::thread::sleep(Duration::from_millis(10));
632+
if let Some(timer) = timer.upgrade() {
633+
// the flag is still in use
634+
if start.elapsed().ge(&limit) {
635+
// set flag and stop polling
636+
timer.flag.store(1, Ordering::Relaxed);
637+
return;
638+
}
639+
} else {
640+
// the flag is not in use anymore, stop polling
641+
return;
642+
}
643+
}
644+
});
645+
result
646+
}
647+
}
648+
649+
impl CancellationFlag for CancelAfterDuration {
650+
fn flag(&self) -> Option<&AtomicUsize> {
651+
Some(&self.flag)
652+
}
653+
}
654+
615655
/// An error that can occur while loading a stack graph from a TSG file
616656
#[derive(Debug, Error)]
617657
pub enum LoadError {

0 commit comments

Comments
 (0)