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

Commit 40c6918

Browse files
Merge pull request #174 from github/analyze-command
2 parents 0ecad51 + 5b2faf6 commit 40c6918

File tree

9 files changed

+449
-51
lines changed

9 files changed

+449
-51
lines changed

tree-sitter-stack-graphs/CHANGELOG.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,24 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## Unreleased
9+
10+
### Library
11+
12+
#### Added
13+
14+
- A new `CancelAfterDuration` implementation of `CancellationFlag` that cancels the computation after a certain amount of time.
15+
16+
#### Changed
17+
18+
- The `LanguageConfiguration::matches_file` method takes a `ContentProvider` instead of an `Option<&str>` value. This allows lazy file reading *after* the filename is checked, instead of the unconditional loading required before. To give content readers the opportunity to cache read values, a mutable reference is required. The return type has changed to `std::io::Result` to propagate possible errors from content providers. A `FileReader` implementation that caches the last read file is provided as well.
19+
20+
### CLI
21+
22+
#### Added
23+
24+
- A new `analyze` command that computes stack graphs and partial paths for all given source files and directories. The command does not produce any output at the moment. Analysis per file can be limited using the `--max-file-time` flag.
25+
826
## v0.6.0 -- 2023-01-13
927

1028
### Library

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

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@
5555
5656
pub(self) const MAX_PARSE_ERRORS: usize = 5;
5757

58+
pub mod analyze;
5859
pub mod init;
5960
pub mod load;
6061
pub mod parse;
@@ -64,13 +65,15 @@ mod util;
6465
pub mod path_loading {
6566
use clap::Subcommand;
6667

68+
use crate::cli::analyze::AnalyzeArgs;
6769
use crate::cli::init::InitArgs;
6870
use crate::cli::load::PathLoaderArgs;
6971
use crate::cli::parse::ParseArgs;
7072
use crate::cli::test::TestArgs;
7173

7274
#[derive(Subcommand)]
7375
pub enum Subcommands {
76+
Analyze(Analyze),
7477
Init(Init),
7578
Parse(Parse),
7679
Test(Test),
@@ -79,13 +82,30 @@ pub mod path_loading {
7982
impl Subcommands {
8083
pub fn run(&self) -> anyhow::Result<()> {
8184
match self {
85+
Self::Analyze(cmd) => cmd.run(),
8286
Self::Init(cmd) => cmd.run(),
8387
Self::Parse(cmd) => cmd.run(),
8488
Self::Test(cmd) => cmd.run(),
8589
}
8690
}
8791
}
8892

93+
/// Analyze command
94+
#[derive(clap::Parser)]
95+
pub struct Analyze {
96+
#[clap(flatten)]
97+
load_args: PathLoaderArgs,
98+
#[clap(flatten)]
99+
analyze_args: AnalyzeArgs,
100+
}
101+
102+
impl Analyze {
103+
pub fn run(&self) -> anyhow::Result<()> {
104+
let mut loader = self.load_args.get()?;
105+
self.analyze_args.run(&mut loader)
106+
}
107+
}
108+
89109
/// Init command
90110
#[derive(clap::Parser)]
91111
pub struct Init {
@@ -135,27 +155,45 @@ pub mod path_loading {
135155
pub mod provided_languages {
136156
use clap::Subcommand;
137157

158+
use crate::cli::analyze::AnalyzeArgs;
159+
use crate::cli::load::LanguageConfigurationsLoaderArgs;
138160
use crate::cli::parse::ParseArgs;
139161
use crate::cli::test::TestArgs;
140162
use crate::loader::LanguageConfiguration;
141163

142-
use super::load::LanguageConfigurationsLoaderArgs;
143-
144164
#[derive(Subcommand)]
145165
pub enum Subcommands {
166+
Analyze(Analyze),
146167
Parse(Parse),
147168
Test(Test),
148169
}
149170

150171
impl Subcommands {
151172
pub fn run(&self, configurations: Vec<LanguageConfiguration>) -> anyhow::Result<()> {
152173
match self {
174+
Self::Analyze(cmd) => cmd.run(configurations),
153175
Self::Parse(cmd) => cmd.run(configurations),
154176
Self::Test(cmd) => cmd.run(configurations),
155177
}
156178
}
157179
}
158180

181+
/// Analyze command
182+
#[derive(clap::Parser)]
183+
pub struct Analyze {
184+
#[clap(flatten)]
185+
load_args: LanguageConfigurationsLoaderArgs,
186+
#[clap(flatten)]
187+
analyze_args: AnalyzeArgs,
188+
}
189+
190+
impl Analyze {
191+
pub fn run(&self, configurations: Vec<LanguageConfiguration>) -> anyhow::Result<()> {
192+
let mut loader = self.load_args.get(configurations)?;
193+
self.analyze_args.run(&mut loader)
194+
}
195+
}
196+
159197
/// Parse command
160198
#[derive(clap::Parser)]
161199
pub struct Parse {
Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
// -*- coding: utf-8 -*-
2+
// ------------------------------------------------------------------------------------------------
3+
// Copyright © 2021, stack-graphs authors.
4+
// Licensed under either of Apache License, Version 2.0, or MIT license, at your option.
5+
// Please see the LICENSE-APACHE or LICENSE-MIT files in this distribution for license details.
6+
// ------------------------------------------------------------------------------------------------
7+
8+
use anyhow::anyhow;
9+
use anyhow::Context as _;
10+
use clap::Args;
11+
use clap::ValueHint;
12+
use stack_graphs::graph::StackGraph;
13+
use stack_graphs::partial::PartialPaths;
14+
use stack_graphs::stitching::Database;
15+
use std::collections::HashMap;
16+
use std::path::Path;
17+
use std::path::PathBuf;
18+
use std::sync::Arc;
19+
use std::time::Duration;
20+
use tree_sitter_graph::Variables;
21+
use walkdir::WalkDir;
22+
23+
use crate::cli::util::duration_from_seconds_str;
24+
use crate::cli::util::map_parse_errors;
25+
use crate::cli::util::path_exists;
26+
use crate::loader::FileReader;
27+
use crate::loader::Loader;
28+
use crate::CancelAfterDuration;
29+
use crate::CancellationFlag;
30+
use crate::LoadError;
31+
use crate::NoCancellation;
32+
33+
use super::util::FileStatusLogger;
34+
35+
/// Analyze sources
36+
#[derive(Args)]
37+
pub struct AnalyzeArgs {
38+
/// Source file or directory paths.
39+
#[clap(
40+
value_name = "SOURCE_PATH",
41+
required = true,
42+
value_hint = ValueHint::AnyPath,
43+
parse(from_os_str),
44+
validator_os = path_exists,
45+
)]
46+
pub source_paths: Vec<PathBuf>,
47+
48+
#[clap(long, short = 'v')]
49+
pub verbose: bool,
50+
51+
/// Maximum runtime per file in seconds.
52+
#[clap(
53+
long,
54+
value_name = "SECONDS",
55+
parse(try_from_str = duration_from_seconds_str),
56+
)]
57+
pub max_file_time: Option<Duration>,
58+
}
59+
60+
impl AnalyzeArgs {
61+
pub fn new(source_paths: Vec<PathBuf>) -> Self {
62+
Self {
63+
source_paths,
64+
verbose: false,
65+
max_file_time: None,
66+
}
67+
}
68+
69+
pub fn run(&self, loader: &mut Loader) -> anyhow::Result<()> {
70+
for source_path in &self.source_paths {
71+
if source_path.is_dir() {
72+
let source_root = source_path;
73+
for source_entry in WalkDir::new(source_root)
74+
.follow_links(true)
75+
.into_iter()
76+
.filter_map(|e| e.ok())
77+
.filter(|e| e.file_type().is_file())
78+
{
79+
let source_path = source_entry.path();
80+
self.analyze_file_with_context(source_root, source_path, loader)?;
81+
}
82+
} else {
83+
let source_root = source_path.parent().unwrap();
84+
self.analyze_file_with_context(source_root, source_path, loader)?;
85+
}
86+
}
87+
Ok(())
88+
}
89+
90+
/// Analyze file and add error context to any failures that are returned.
91+
fn analyze_file_with_context(
92+
&self,
93+
source_root: &Path,
94+
source_path: &Path,
95+
loader: &mut Loader,
96+
) -> anyhow::Result<()> {
97+
self.analyze_file(source_root, source_path, loader)
98+
.with_context(|| format!("Error analyzing file {}", source_path.display()))
99+
}
100+
101+
fn analyze_file(
102+
&self,
103+
source_root: &Path,
104+
source_path: &Path,
105+
loader: &mut Loader,
106+
) -> anyhow::Result<()> {
107+
let mut file_status = FileStatusLogger::new(source_path, self.verbose);
108+
109+
let mut file_reader = FileReader::new();
110+
let lc = match loader.load_for_file(source_path, &mut file_reader, &NoCancellation) {
111+
Ok(Some(sgl)) => sgl,
112+
Ok(None) => return Ok(()),
113+
Err(crate::loader::LoadError::Cancelled(_)) => {
114+
file_status.warn("language loading timed out")?;
115+
return Ok(());
116+
}
117+
Err(e) => return Err(e.into()),
118+
};
119+
let source = file_reader.get(source_path)?;
120+
121+
let mut cancellation_flag: Arc<dyn CancellationFlag> = Arc::new(NoCancellation);
122+
if let Some(max_file_time) = self.max_file_time {
123+
cancellation_flag = CancelAfterDuration::new(max_file_time);
124+
}
125+
126+
file_status.processing()?;
127+
128+
let mut graph = StackGraph::new();
129+
let file = match graph.add_file(&source_path.to_string_lossy()) {
130+
Ok(file) => file,
131+
Err(_) => return Err(anyhow!("Duplicate file {}", source_path.display())),
132+
};
133+
134+
let relative_source_path = source_path.strip_prefix(source_root).unwrap();
135+
let result = if let Some(fa) = source_path
136+
.file_name()
137+
.and_then(|f| lc.special_files.get(&f.to_string_lossy()))
138+
{
139+
fa.build_stack_graph_into(
140+
&mut graph,
141+
file,
142+
&relative_source_path,
143+
&source,
144+
&mut std::iter::empty(),
145+
&HashMap::new(),
146+
cancellation_flag.as_ref(),
147+
)
148+
} else {
149+
let globals = Variables::new();
150+
lc.sgl.build_stack_graph_into(
151+
&mut graph,
152+
file,
153+
&source,
154+
&globals,
155+
cancellation_flag.as_ref(),
156+
)
157+
};
158+
match result {
159+
Err(LoadError::ParseErrors(parse_errors)) => {
160+
let parse_error = map_parse_errors(source_path, &parse_errors, &source, "");
161+
file_status.error("parsing failed")?;
162+
eprintln!("{}", parse_error);
163+
return Ok(());
164+
}
165+
Err(LoadError::Cancelled(_)) => {
166+
file_status.warn("parsing timed out")?;
167+
return Ok(());
168+
}
169+
Err(e) => return Err(e.into()),
170+
Ok(_) => {}
171+
};
172+
173+
let mut partials = PartialPaths::new();
174+
let mut db = Database::new();
175+
match partials.find_all_partial_paths_in_file(
176+
&graph,
177+
file,
178+
&cancellation_flag.as_ref(),
179+
|g, ps, p| {
180+
if p.is_complete_as_possible(g) {
181+
db.add_partial_path(g, ps, p);
182+
}
183+
},
184+
) {
185+
Ok(_) => {}
186+
Err(_) => {
187+
file_status.warn("path computation timed out")?;
188+
return Ok(());
189+
}
190+
}
191+
192+
file_status.ok("success")?;
193+
Ok(())
194+
}
195+
}

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ use tree_sitter::Parser;
1515
use tree_sitter_graph::parse_error::ParseError;
1616

1717
use crate::cli::util::path_exists;
18+
use crate::loader::FileReader;
1819
use crate::loader::Loader;
1920
use crate::LoadError;
2021

@@ -34,11 +35,12 @@ impl ParseArgs {
3435
}
3536

3637
fn parse_file(&self, file_path: &Path, loader: &mut Loader) -> anyhow::Result<()> {
37-
let source = std::fs::read_to_string(file_path)?;
38-
let lang = match loader.load_tree_sitter_language_for_file(file_path, Some(&source))? {
38+
let mut file_reader = FileReader::new();
39+
let lang = match loader.load_tree_sitter_language_for_file(file_path, &mut file_reader)? {
3940
Some(sgl) => sgl,
4041
None => return Err(anyhow!("No stack graph language found")),
4142
};
43+
let source = file_reader.get(file_path)?;
4244

4345
let mut parser = Parser::new();
4446
parser.set_language(lang)?;

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

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ use walkdir::WalkDir;
2424
use crate::cli::util::map_parse_errors;
2525
use crate::cli::util::path_exists;
2626
use crate::cli::util::PathSpec;
27+
use crate::loader::FileReader;
2728
use crate::loader::LanguageConfiguration;
2829
use crate::loader::Loader;
2930
use crate::test::Test;
@@ -193,8 +194,8 @@ impl TestArgs {
193194
test_path: &Path,
194195
loader: &mut Loader,
195196
) -> anyhow::Result<TestResult> {
196-
let source = std::fs::read_to_string(test_path)?;
197-
let lc = match loader.load_for_file(test_path, Some(&source), &NoCancellation)? {
197+
let mut file_reader = FileReader::new();
198+
let lc = match loader.load_for_file(test_path, &mut file_reader, &NoCancellation)? {
198199
Some(sgl) => sgl,
199200
None => {
200201
if self.show_ignored {
@@ -203,6 +204,7 @@ impl TestArgs {
203204
return Ok(TestResult::new());
204205
}
205206
};
207+
let source = file_reader.get(test_path)?;
206208
let default_fragment_path = test_path.strip_prefix(test_root).unwrap();
207209
let mut test = Test::from_source(&test_path, &source, default_fragment_path)?;
208210
self.load_builtins_into(&lc, &mut test.graph)
@@ -224,7 +226,10 @@ impl TestArgs {
224226
&test_fragment.globals,
225227
&NoCancellation,
226228
)?;
227-
} else if lc.matches_file(&test_fragment.path, Some(&test_fragment.source)) {
229+
} else if lc.matches_file(
230+
&test_fragment.path,
231+
&mut Some(test_fragment.source.as_ref()),
232+
)? {
228233
globals.clear();
229234
test_fragment.add_globals_to(&mut globals);
230235
self.build_fragment_stack_graph_into(
@@ -281,7 +286,7 @@ impl TestArgs {
281286
) -> anyhow::Result<()> {
282287
match sgl.build_stack_graph_into(graph, file, source, globals, &NoCancellation) {
283288
Err(LoadError::ParseErrors(parse_errors)) => {
284-
Err(map_parse_errors(test_path, &parse_errors, source))
289+
Err(map_parse_errors(test_path, &parse_errors, source, " "))
285290
}
286291
Err(e) => Err(e.into()),
287292
Ok(_) => Ok(()),

0 commit comments

Comments
 (0)