feat(cli): allow users to specify dynamic libraries directly for

parse, query, test, and fuzz subcommands
This commit is contained in:
Will Lillis 2025-07-26 20:06:42 -04:00
parent 5e1daf0c41
commit acfeed006a
2 changed files with 164 additions and 29 deletions

View file

@ -185,6 +185,13 @@ struct Parse {
/// The path to the tree-sitter grammar directory
#[arg(long, short = 'p')]
pub grammar_path: Option<PathBuf>,
/// The path to the parser's dynamic library
#[arg(long, short = 'l')]
pub lib_path: Option<PathBuf>,
/// If `--lib_path` is used, the name of the language used to extract the
/// library's language function
#[arg(long)]
pub lang_name: Option<String>,
/// Select a language by the scope instead of a file extension
#[arg(long)]
pub scope: Option<String>,
@ -276,6 +283,13 @@ struct Test {
/// The path to the tree-sitter grammar directory
#[arg(long, short = 'p')]
pub grammar_path: Option<PathBuf>,
/// The path to the parser's dynamic library
#[arg(long, short = 'l')]
pub lib_path: Option<PathBuf>,
/// If `--lib_path` is used, the name of the language used to extract the
/// library's language function
#[arg(long)]
pub lang_name: Option<String>,
/// Update all syntax trees in corpus files with current parser output
#[arg(long, short)]
pub update: bool,
@ -335,6 +349,13 @@ struct Fuzz {
/// The path to the tree-sitter grammar directory
#[arg(long, short = 'p')]
pub grammar_path: Option<PathBuf>,
/// The path to the parser's dynamic library
#[arg(long)]
pub lib_path: Option<PathBuf>,
/// If `--lib_path` is used, the name of the language used to extract the
/// library's language function
#[arg(long)]
pub lang_name: Option<String>,
/// Maximum number of edits to perform per fuzz test
#[arg(long)]
pub edits: Option<usize>,
@ -367,6 +388,13 @@ struct Query {
/// The path to the tree-sitter grammar directory
#[arg(long, short = 'p')]
pub grammar_path: Option<PathBuf>,
/// The path to the parser's dynamic library
#[arg(long, short = 'l')]
pub lib_path: Option<PathBuf>,
/// If `--lib_path` is used, the name of the language used to extract the
/// library's language function
#[arg(long)]
pub lang_name: Option<String>,
/// Measure execution time
#[arg(long, short)]
pub time: bool,
@ -1007,6 +1035,11 @@ impl Parse {
has_error |= !parse_result.successful;
};
if self.lib_path.is_none() && self.lang_name.is_some() {
eprintln!("Warning: --lang-name` specified without --lib-path. This argument will be ignored.");
}
let lib_info = get_lib_info(self.lib_path.as_ref(), self.lang_name.as_ref());
let input = get_input(
self.paths_file.as_deref(),
self.paths,
@ -1024,7 +1057,7 @@ impl Parse {
for path in &paths {
let path = Path::new(&path);
let language = loader
.select_language(path, current_dir, self.scope.as_deref())
.select_language(path, current_dir, self.scope.as_deref(), lib_info)
.with_context(|| {
anyhow!("Failed to load langauge for path \"{}\"", path.display())
})?;
@ -1048,16 +1081,29 @@ impl Parse {
} => {
let path = get_tmp_source_file(&contents)?;
let languages = loader.languages_at_path(current_dir)?;
let language = languages
.iter()
.find(|(_, n)| language_names.contains(&Box::from(n.as_str())))
.or_else(|| languages.first())
.map(|(l, _)| l.clone())
.ok_or_else(|| anyhow!("No language found"))?;
let language = if let Some(ref lib_path) = self.lib_path {
let lib_info = get_lib_info(self.lib_path.as_ref(), self.lang_name.as_ref());
&loader
.select_language(lib_path, current_dir, None, lib_info)
.with_context(|| {
anyhow!(
"Failed to load language for path \"{}\"",
lib_path.display()
)
})?
} else {
&languages
.iter()
.find(|(_, n)| language_names.contains(&Box::from(n.as_str())))
.or_else(|| languages.first())
.map(|(l, _)| l.clone())
.ok_or_else(|| anyhow!("No language found"))?
};
parse::parse_file_at_path(
&mut parser,
&language,
language,
&path,
&name,
name.chars().count(),
@ -1073,7 +1119,7 @@ impl Parse {
let path = get_tmp_source_file(&contents)?;
let name = "stdin";
let language = loader.select_language(&path, current_dir, None)?;
let language = loader.select_language(&path, current_dir, None, lib_info)?;
parse::parse_file_at_path(
&mut parser,
@ -1123,11 +1169,26 @@ impl Test {
loader.use_wasm(&engine);
}
if self.lib_path.is_none() && self.lang_name.is_some() {
eprintln!("Warning: --lang-name` specified without --lib-path. This argument will be ignored.");
}
let languages = loader.languages_at_path(current_dir)?;
let language = &languages
.first()
.ok_or_else(|| anyhow!("No language found"))?
.0;
let language = if let Some(ref lib_path) = self.lib_path {
let lib_info = get_lib_info(self.lib_path.as_ref(), self.lang_name.as_ref());
&loader
.select_language(lib_path, current_dir, None, lib_info)
.with_context(|| {
anyhow!(
"Failed to load language for path \"{}\"",
lib_path.display()
)
})?
} else {
&languages
.first()
.ok_or_else(|| anyhow!("No language found"))?
.0
};
parser.set_language(language)?;
let test_dir = current_dir.join("test");
@ -1253,10 +1314,29 @@ impl Fuzz {
loader.sanitize_build(true);
loader.force_rebuild(self.rebuild);
if self.lib_path.is_none() && self.lang_name.is_some() {
eprintln!("Warning: --lang-name` specified without --lib-path. This argument will be ignored.");
}
let languages = loader.languages_at_path(current_dir)?;
let (language, language_name) = &languages
.first()
.ok_or_else(|| anyhow!("No language found"))?;
let (language, language_name) = if let Some(ref lib_path) = self.lib_path {
let lib_info = get_lib_info(self.lib_path.as_ref(), self.lang_name.as_ref())
.with_context(|| anyhow!("No language name found for {}", lib_path.display()))?;
&(
loader
.select_language(lib_path, current_dir, None, Some(lib_info))
.with_context(|| {
anyhow!(
"Failed to load language for path \"{}\"",
lib_path.display()
)
})?,
lib_info.1.to_string(),
)
} else {
languages
.first()
.ok_or_else(|| anyhow!("No language found"))?
};
let mut fuzz_options = FuzzOptions {
skipped: self.skip,
@ -1302,6 +1382,13 @@ impl Query {
let cancellation_flag = util::cancel_on_signal();
if self.lib_path.is_none() && self.lang_name.is_some() {
eprintln!(
"Warning: --lang-name specified without --lib-path. This argument will be ignored."
);
}
let lib_info = get_lib_info(self.lib_path.as_ref(), self.lang_name.as_ref());
let input = get_input(
self.paths_file.as_deref(),
self.paths,
@ -1315,6 +1402,7 @@ impl Query {
Path::new(&paths[0]),
current_dir,
self.scope.as_deref(),
lib_info,
)?;
for path in paths {
@ -1340,14 +1428,26 @@ impl Query {
} => {
let path = get_tmp_source_file(&contents)?;
let languages = loader.languages_at_path(current_dir)?;
let language = languages
.iter()
.find(|(_, n)| language_names.contains(&Box::from(n.as_str())))
.or_else(|| languages.first())
.map(|(l, _)| l.clone())
.ok_or_else(|| anyhow!("No language found"))?;
let language = if let Some(ref lib_path) = self.lib_path {
let lib_info = get_lib_info(self.lib_path.as_ref(), self.lang_name.as_ref());
&loader
.select_language(lib_path, current_dir, None, lib_info)
.with_context(|| {
anyhow!(
"Failed to load language for path \"{}\"",
lib_path.display()
)
})?
} else {
&languages
.iter()
.find(|(_, n)| language_names.contains(&Box::from(n.as_str())))
.or_else(|| languages.first())
.map(|(l, _)| l.clone())
.ok_or_else(|| anyhow!("No language found"))?
};
query::query_file_at_path(
&language,
language,
&path,
&name,
query_path,
@ -1366,7 +1466,7 @@ impl Query {
println!();
let path = get_tmp_source_file(&contents)?;
let language = loader.select_language(&path, current_dir, None)?;
let language = loader.select_language(&path, current_dir, None, lib_info)?;
query::query_file_at_path(
&language,
&path,
@ -1841,3 +1941,24 @@ const fn get_styles() -> clap::builder::Styles {
)
.placeholder(Style::new().fg_color(Some(Color::Ansi(AnsiColor::White))))
}
/// Utility to extract the shared library path and language function name from user-provided
/// arguments if present.
fn get_lib_info<'a>(
lib_path: Option<&'a PathBuf>,
language_name: Option<&'a String>,
) -> Option<(&'a Path, &'a str)> {
if let Some(lib_path) = lib_path {
// Use the user-specified name if present, otherwise try to derive it from
// the lib path
match (
language_name.map(|s| s.as_str()),
lib_path.file_stem().and_then(|s| s.to_str()),
) {
(Some(name), _) | (None, Some(name)) => Some((lib_path.as_path(), name)),
_ => None,
}
} else {
None
}
}

View file

@ -849,12 +849,21 @@ impl Loader {
}
}
let library = unsafe { Library::new(&output_path) }
.with_context(|| format!("Error opening dynamic library {}", output_path.display()))?;
Self::load_language(&output_path, &language_fn_name)
}
pub fn load_language(path: &Path, function_name: &str) -> Result<Language> {
let library = unsafe { Library::new(path) }
.with_context(|| format!("Error opening dynamic library {}", path.display()))?;
let language = unsafe {
let language_fn = library
.get::<Symbol<unsafe extern "C" fn() -> Language>>(language_fn_name.as_bytes())
.with_context(|| format!("Failed to load symbol {language_fn_name}"))?;
.get::<Symbol<unsafe extern "C" fn() -> Language>>(function_name.as_bytes())
.with_context(|| {
format!(
"Failed to load symbol {function_name} from {}",
path.display()
)
})?;
language_fn()
};
mem::forget(library);
@ -1410,8 +1419,13 @@ impl Loader {
path: &Path,
current_dir: &Path,
scope: Option<&str>,
// path to dynamic library, name of language
lib_info: Option<(&Path, &str)>,
) -> Result<Language> {
if let Some(scope) = scope {
if let Some((lib_path, language_name)) = lib_info {
let language_fn_name = format!("tree_sitter_{}", language_name.replace('-', "_"));
Self::load_language(lib_path, &language_fn_name)
} else if let Some(scope) = scope {
if let Some(config) = self
.language_configuration_for_scope(scope)
.with_context(|| format!("Failed to load language for scope '{scope}'"))?