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

Commit b500136

Browse files
author
Hendrik van Antwerpen
authored
Merge pull request #272 from github/match-command
Add `match` command to run stanza queries
2 parents 5722b5a + 3ec6054 commit b500136

File tree

5 files changed

+287
-80
lines changed

5 files changed

+287
-80
lines changed

tree-sitter-stack-graphs/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2626
#### Added
2727

2828
- 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.
29+
- A new `match` command executes the query patterns from the TSG source and outputs the matches with captured nodes to the console. The `--stanza/-S` flag can be used to select specific stanzas to match by giving the line number where the stanza appears in the source. (Any line that is part of the stanza will work.)
2930

3031
#### Changed
3132

tree-sitter-stack-graphs/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ tokio = { version = "1.26", optional = true, features = ["io-std", "rt", "rt-mul
7676
tower-lsp = { version = "0.19", optional = true }
7777
tree-sitter = ">= 0.19"
7878
tree-sitter-config = { version = "0.19", optional = true }
79-
tree-sitter-graph = "0.10"
79+
tree-sitter-graph = "0.10.1"
8080
tree-sitter-loader = "0.20"
8181
walkdir = { version = "2.3", optional = true }
8282

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

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ pub mod init;
6464
pub mod load;
6565
#[cfg(feature = "lsp")]
6666
pub mod lsp;
67+
pub mod r#match;
6768
pub mod parse;
6869
pub mod query;
6970
pub mod status;
@@ -83,6 +84,7 @@ pub mod path_loading {
8384
use crate::cli::lsp::LspArgs;
8485
use crate::cli::parse::ParseArgs;
8586
use crate::cli::query::QueryArgs;
87+
use crate::cli::r#match::MatchArgs;
8688
use crate::cli::status::StatusArgs;
8789
use crate::cli::test::TestArgs;
8890

@@ -95,6 +97,7 @@ pub mod path_loading {
9597
Init(Init),
9698
#[cfg(feature = "lsp")]
9799
Lsp(Lsp),
100+
Match(Match),
98101
Parse(Parse),
99102
Query(Query),
100103
Status(Status),
@@ -109,6 +112,7 @@ pub mod path_loading {
109112
Self::Init(cmd) => cmd.run(),
110113
#[cfg(feature = "lsp")]
111114
Self::Lsp(cmd) => cmd.run(default_db_path),
115+
Self::Match(cmd) => cmd.run(),
112116
Self::Parse(cmd) => cmd.run(),
113117
Self::Query(cmd) => cmd.run(default_db_path),
114118
Self::Status(cmd) => cmd.run(default_db_path),
@@ -186,6 +190,22 @@ pub mod path_loading {
186190
}
187191
}
188192

193+
/// Match stanza queries against a source file.
194+
#[derive(clap::Parser)]
195+
pub struct Match {
196+
#[clap(flatten)]
197+
load_args: PathLoaderArgs,
198+
#[clap(flatten)]
199+
match_args: MatchArgs,
200+
}
201+
202+
impl Match {
203+
pub fn run(self) -> anyhow::Result<()> {
204+
let loader = self.load_args.get()?;
205+
self.match_args.run(loader)
206+
}
207+
}
208+
189209
/// Parse a source file and show the parse tree.
190210
#[derive(clap::Parser)]
191211
pub struct Parse {
@@ -264,6 +284,7 @@ pub mod provided_languages {
264284
use crate::cli::lsp::LspArgs;
265285
use crate::cli::parse::ParseArgs;
266286
use crate::cli::query::QueryArgs;
287+
use crate::cli::r#match::MatchArgs;
267288
use crate::cli::status::StatusArgs;
268289
use crate::cli::test::TestArgs;
269290
use crate::loader::LanguageConfiguration;
@@ -277,6 +298,7 @@ pub mod provided_languages {
277298
Init(Init),
278299
#[cfg(feature = "lsp")]
279300
Lsp(Lsp),
301+
Match(Match),
280302
Parse(Parse),
281303
Query(Query),
282304
Status(Status),
@@ -295,6 +317,7 @@ pub mod provided_languages {
295317
Self::Init(cmd) => cmd.run(),
296318
#[cfg(feature = "lsp")]
297319
Self::Lsp(cmd) => cmd.run(default_db_path, configurations),
320+
Self::Match(cmd) => cmd.run(configurations),
298321
Self::Parse(cmd) => cmd.run(configurations),
299322
Self::Query(cmd) => cmd.run(default_db_path),
300323
Self::Status(cmd) => cmd.run(default_db_path),
@@ -380,6 +403,22 @@ pub mod provided_languages {
380403
}
381404
}
382405

406+
/// Match stanza queries against a source file.
407+
#[derive(clap::Parser)]
408+
pub struct Match {
409+
#[clap(flatten)]
410+
load_args: LanguageConfigurationsLoaderArgs,
411+
#[clap(flatten)]
412+
match_args: MatchArgs,
413+
}
414+
415+
impl Match {
416+
pub fn run(self, configurations: Vec<LanguageConfiguration>) -> anyhow::Result<()> {
417+
let loader = self.load_args.get(configurations)?;
418+
self.match_args.run(loader)
419+
}
420+
}
421+
383422
/// Parse a source file and show the parse tree.
384423
#[derive(clap::Parser)]
385424
pub struct Parse {
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
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 clap::Args;
10+
use clap::ValueHint;
11+
use colored::Colorize;
12+
use std::path::Path;
13+
use std::path::PathBuf;
14+
use tree_sitter::CaptureQuantifier;
15+
use tree_sitter::Node;
16+
17+
use crate::cli::parse::parse;
18+
use crate::cli::parse::print_node;
19+
use crate::cli::util::ExistingPathBufValueParser;
20+
use crate::loader::FileReader;
21+
use crate::loader::Loader;
22+
use crate::NoCancellation;
23+
24+
/// Match file
25+
#[derive(Args)]
26+
pub struct MatchArgs {
27+
/// Input file path.
28+
#[clap(
29+
value_name = "SOURCE_PATH",
30+
required = true,
31+
value_hint = ValueHint::AnyPath,
32+
value_parser = ExistingPathBufValueParser,
33+
)]
34+
pub source_path: PathBuf,
35+
36+
/// Only match stanza on the given line.
37+
#[clap(long, value_name = "LINE_NUMBER", short = 'S')]
38+
pub stanza: Vec<usize>,
39+
}
40+
41+
impl MatchArgs {
42+
pub fn run(self, mut loader: Loader) -> anyhow::Result<()> {
43+
let mut file_reader = FileReader::new();
44+
let lc = match loader.load_for_file(&self.source_path, &mut file_reader, &NoCancellation)? {
45+
Some(lc) => lc,
46+
None => return Err(anyhow!("No stack graph language found")),
47+
};
48+
let source = file_reader.get(&self.source_path)?;
49+
let tree = parse(lc.language, &self.source_path, source)?;
50+
if self.stanza.is_empty() {
51+
lc.sgl.tsg.try_visit_matches(&tree, source, true, |mat| {
52+
print_matches(lc.sgl.tsg_path(), &self.source_path, source, mat)
53+
})?;
54+
} else {
55+
for line in &self.stanza {
56+
let stanza = lc
57+
.sgl
58+
.tsg
59+
.stanzas
60+
.iter()
61+
.find(|s| s.range.start.row <= line - 1 && line - 1 <= s.range.end.row)
62+
.ok_or_else(|| {
63+
anyhow!("No stanza on {}:{}", lc.sgl.tsg_path().display(), line)
64+
})?;
65+
stanza.try_visit_matches(&tree, source, |mat| {
66+
print_matches(lc.sgl.tsg_path(), &self.source_path, source, mat)
67+
})?;
68+
}
69+
}
70+
Ok(())
71+
}
72+
}
73+
74+
fn print_matches(
75+
tsg_path: &Path,
76+
source_path: &Path,
77+
source: &str,
78+
mat: tree_sitter_graph::Match,
79+
) -> anyhow::Result<()> {
80+
println!(
81+
"{}: stanza query",
82+
format!(
83+
"{}:{}:{}",
84+
tsg_path.display(),
85+
mat.query_location().row + 1,
86+
mat.query_location().column + 1
87+
)
88+
.bold(),
89+
);
90+
{
91+
let full_capture = mat.full_capture();
92+
print!(" matched ");
93+
print_node(full_capture, true);
94+
print_node_text(full_capture, source_path, source)?;
95+
println!();
96+
}
97+
let width = mat
98+
.capture_names()
99+
.map(|n| n.len())
100+
.max()
101+
.unwrap_or_default();
102+
if width == 0 {
103+
return Ok(());
104+
}
105+
println!(" and captured");
106+
for (name, quantifier, nodes) in mat.named_captures() {
107+
for (idx, node) in nodes.enumerate() {
108+
if idx == 0 {
109+
print!(
110+
" @{}{}{} = ",
111+
name,
112+
quantifier_ch(quantifier),
113+
" ".repeat(width - name.len())
114+
);
115+
} else {
116+
print!(" {} | ", " ".repeat(width));
117+
}
118+
print_node(node, true);
119+
print_node_text(node, source_path, source)?;
120+
println!();
121+
}
122+
}
123+
Ok(())
124+
}
125+
126+
fn print_node_text(node: Node, source_path: &Path, source: &str) -> anyhow::Result<()> {
127+
const MAX_TEXT_LENGTH: usize = 16;
128+
129+
print!(", text: \"");
130+
let text = node.utf8_text(source.as_bytes())?;
131+
let summary: String = text
132+
.chars()
133+
.take(MAX_TEXT_LENGTH)
134+
.take_while(|c| *c != '\n')
135+
.collect();
136+
print!("{}", summary.blue());
137+
if summary.len() < text.len() {
138+
print!("{}", "…".dimmed());
139+
}
140+
print!("\"");
141+
print!(
142+
", path: {}:{}:{}",
143+
source_path.display(),
144+
node.start_position().row + 1,
145+
node.start_position().column + 1
146+
);
147+
Ok(())
148+
}
149+
150+
fn quantifier_ch(quantifier: CaptureQuantifier) -> char {
151+
match quantifier {
152+
CaptureQuantifier::Zero => '-',
153+
CaptureQuantifier::ZeroOrOne => '?',
154+
CaptureQuantifier::ZeroOrMore => '*',
155+
CaptureQuantifier::One => ' ',
156+
CaptureQuantifier::OneOrMore => '+',
157+
}
158+
}

0 commit comments

Comments
 (0)