Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions diffr/crates/diffr-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -137,3 +137,7 @@ toml = "0.8"
[features]
# Constructors and helpers the diffr package's tests build on.
test-support = []

[[example]]
name = "initialization"
path = "src/initialization_example.rs"
139 changes: 101 additions & 38 deletions diffr/crates/diffr-core/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ use crate::options::DiffOptions;
use crate::parse::{guess_language::Language, tree_sitter_parser};
use crate::plugin::config::PluginsConfig;
use crate::plugin::queries::{self, Queries};
use query::AnnotationQuery;
use query::{AnnotationQuery, QuerySource};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};
Expand Down Expand Up @@ -128,7 +128,7 @@ impl Default for ThemeConfig {
}
}

#[derive(Debug)]
#[derive(Debug, Clone)]
pub struct ConfigError(pub(crate) String);
impl std::fmt::Display for ConfigError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
Expand All @@ -138,7 +138,9 @@ impl std::fmt::Display for ConfigError {
impl std::error::Error for ConfigError {}

pub struct Params {
languages: DftHashMap<Language, OnceLock<Arc<LanguageParams>>>,
languages: DftHashMap<Language, OnceLock<Result<Arc<LanguageParams>, ConfigError>>>,
sources: DftHashMap<Language, Vec<QuerySource>>,
plugin_order: Vec<String>,
pub diff: DiffConfig,
}

Expand All @@ -147,10 +149,13 @@ pub struct LanguageParams {
/// The fold and context queries of every enabled plugin, concatenated.
pub(crate) query: AnnotationQuery,
sub_languages: OnceLock<
Vec<(
&'static tree_sitter_parser::TreeSitterSubLanguage,
Arc<LanguageParams>,
)>,
Result<
Vec<(
&'static tree_sitter_parser::TreeSitterSubLanguage,
Arc<LanguageParams>,
)>,
ConfigError,
>,
>,
}

Expand All @@ -161,7 +166,11 @@ impl LanguageParams {
&'static tree_sitter_parser::TreeSitterSubLanguage,
Arc<LanguageParams>,
)] {
self.sub_languages.get().expect("resolved sub-languages")
self.sub_languages
.get()
.expect("resolved sub-languages")
.as_ref()
.expect("valid sub-languages")
}
}

Expand Down Expand Up @@ -266,27 +275,34 @@ impl Config {
/// Compile with `queries`, the enabled plugins' query files in
/// `plugins.shape.order`.
pub fn compile_queries(&self, queries: Vec<(String, Queries)>) -> Result<Params, ConfigError> {
let mut languages: DftHashMap<_, _> = Language::iter()
let params = self.prepare_queries(queries)?;
for &language in params.sources.keys() {
params.compiled_language(language)?;
}
Ok(params)
}

/// Assemble query sources now, compiling each language on first use. Hosts
/// using this path report query errors with the file that needs them.
pub fn prepare(&self) -> Result<Params, ConfigError> {
self.prepare_queries(self.plugins.shape.queries()?)
}

fn prepare_queries(&self, queries: Vec<(String, Queries)>) -> Result<Params, ConfigError> {
let languages: DftHashMap<_, _> = Language::iter()
.map(|language| (language, OnceLock::new()))
.collect();
let mut by_language = DftHashMap::default();
for (name, sources) in queries::assemble(&queries)? {
let language = Language::iter()
.find(|language| format!("{language:?}").to_lowercase() == name)
.ok_or_else(|| ConfigError(format!("unknown language: {name}")))?;
let parser = tree_sitter_parser::from_language(language);
let query = AnnotationQuery::compile(&parser.language, &sources)?;
check_tags(&query, &self.plugins.shape.order)?;
languages.insert(
language,
OnceLock::from(Arc::new(LanguageParams {
parser,
query,
sub_languages: OnceLock::new(),
})),
);
by_language.insert(language, sources);
}
Ok(Params {
languages,
sources: by_language,
plugin_order: self.plugins.shape.order.clone(),
diff: self.diff,
})
}
Expand Down Expand Up @@ -314,26 +330,49 @@ fn check_tags(query: &AnnotationQuery, order: &[String]) -> Result<(), ConfigErr
}

impl Params {
pub fn language(&self, language: Language) -> &Arc<LanguageParams> {
let config = self.languages[&language].get_or_init(|| {
// Languages without annotation rules still support structural diffing.
// Keep their grammars lazy, as in the existing parser registry.
let parser = tree_sitter_parser::from_language(language);
Arc::new(LanguageParams {
parser,
query: AnnotationQuery::compile(&parser.language, &[]).expect("an empty query"),
sub_languages: OnceLock::new(),
fn compiled_language(&self, language: Language) -> Result<&Arc<LanguageParams>, ConfigError> {
self.languages[&language]
.get_or_init(|| {
// Languages without annotation rules still support structural diffing.
// Keep their grammars lazy, as in the existing parser registry.
let parser = tree_sitter_parser::from_language(language);
let sources = self
.sources
.get(&language)
.map(Vec::as_slice)
.unwrap_or(&[]);
let query = AnnotationQuery::compile(&parser.language, sources)?;
check_tags(&query, &self.plugin_order)?;
Ok(Arc::new(LanguageParams {
parser,
query,
sub_languages: OnceLock::new(),
}))
})
});
config.sub_languages.get_or_init(|| {
config
.parser
.sub_languages
.iter()
.map(|sub| (sub, Arc::clone(self.language(sub.parse_as))))
.collect()
});
.as_ref()
.map_err(Clone::clone)
}

/// Resolve this language and its embedded languages once. Prepared configs
/// can report query errors here; both successes and failures are cached.
pub fn language(&self, language: Language) -> Result<&Arc<LanguageParams>, ConfigError> {
let config = self.compiled_language(language)?;
config
.sub_languages
.get_or_init(|| {
config
.parser
.sub_languages
.iter()
.map(|sub| {
self.language(sub.parse_as)
.map(|params| (sub, Arc::clone(params)))
})
.collect()
})
.as_ref()
.map_err(Clone::clone)?;
Ok(config)
}
}

Expand All @@ -351,6 +390,30 @@ mod tests {
use super::*;
use crate::summary::DiffResult;

#[test]
fn prepared_queries_fail_only_when_their_language_is_used() {
let queries = vec![(
"removed-runs".to_owned(),
vec![crate::plugin::queries::PluginQuery {
language: "rust".into(),
name: "broken-rust.scm".into(),
text: "(not_a_rust_node) @fold".into(),
}],
)];
let config = Config::default();
assert!(config.compile_queries(queries.clone()).is_err());
let params = config.prepare_queries(queries).unwrap();
assert!(
DiffResult::try_from_sources_with_params("a.py", "x = 1", "x = 2", &params).is_ok()
);
for _ in 0..2 {
let error =
DiffResult::try_from_sources_with_params("a.rs", "fn a() {}", "fn b() {}", &params)
.unwrap_err();
assert!(error.to_string().contains("broken-rust.scm"));
}
}

#[test]
fn diff_limits_default_and_layer_from_the_file() {
let defaults = Config::default().diff;
Expand Down
8 changes: 6 additions & 2 deletions diffr/crates/diffr-core/src/diff/sliders.rs
Original file line number Diff line number Diff line change
Expand Up @@ -800,7 +800,9 @@ mod tests {
fn test_slider_two_steps() {
let arena = Arena::new();
let params = Params::default();
let config = params.language(guess_language::Language::EmacsLisp);
let config = params
.language(guess_language::Language::EmacsLisp)
.unwrap();

let lhs = parse(&arena, "A B", config, false).unwrap();
let rhs = parse(&arena, "A B X\n A B", config, false).unwrap();
Expand All @@ -826,7 +828,9 @@ mod tests {
fn test_slider_partially_unchanged() {
let arena = Arena::new();
let params = Params::default();
let config = params.language(guess_language::Language::EmacsLisp);
let config = params
.language(guess_language::Language::EmacsLisp)
.unwrap();

let lhs = parse(&arena, "(A B) X \n (A B)", config, false).unwrap();
let rhs = parse(&arena, "((novel) A B)", config, false).unwrap();
Expand Down
52 changes: 39 additions & 13 deletions diffr/crates/diffr-core/src/diff/unchanged.rs
Original file line number Diff line number Diff line change
Expand Up @@ -497,7 +497,9 @@ mod tests {
fn test_shrink_unchanged_at_start() {
let arena = Arena::new();
let params = Params::default();
let config = params.language(guess_language::Language::EmacsLisp);
let config = params
.language(guess_language::Language::EmacsLisp)
.unwrap();

let lhs_nodes = parse(&arena, "unchanged A B", config, false).unwrap();
let rhs_nodes = parse(&arena, "unchanged X", config, false).unwrap();
Expand All @@ -524,7 +526,9 @@ mod tests {
fn test_shrink_unchanged_at_end() {
let arena = Arena::new();
let params = Params::default();
let config = params.language(guess_language::Language::EmacsLisp);
let config = params
.language(guess_language::Language::EmacsLisp)
.unwrap();

let lhs_nodes = parse(&arena, "A B unchanged", config, false).unwrap();
let rhs_nodes = parse(&arena, "X unchanged", config, false).unwrap();
Expand All @@ -551,7 +555,9 @@ mod tests {
fn test_shrink_unchanged_nested() {
let arena = Arena::new();
let params = Params::default();
let config = params.language(guess_language::Language::EmacsLisp);
let config = params
.language(guess_language::Language::EmacsLisp)
.unwrap();

let lhs_nodes = parse(
&arena,
Expand Down Expand Up @@ -589,7 +595,9 @@ mod tests {
fn test_split_unchanged_toplevel_at_start() {
let arena = Arena::new();
let params = Params::default();
let config = params.language(guess_language::Language::EmacsLisp);
let config = params
.language(guess_language::Language::EmacsLisp)
.unwrap();

// Make sure that the initial unchanged node exceeds TINY_TREE_THRESHOLD.
let lhs_nodes = parse(
Expand Down Expand Up @@ -631,7 +639,9 @@ mod tests {
fn test_split_unchanged_toplevel_at_end() {
let arena = Arena::new();
let params = Params::default();
let config = params.language(guess_language::Language::EmacsLisp);
let config = params
.language(guess_language::Language::EmacsLisp)
.unwrap();

let lhs_nodes = parse(
&arena,
Expand Down Expand Up @@ -672,7 +682,9 @@ mod tests {
fn test_split_preserves_outer_delimiters() {
let arena = Arena::new();
let params = Params::default();
let config = params.language(guess_language::Language::EmacsLisp);
let config = params
.language(guess_language::Language::EmacsLisp)
.unwrap();

let lhs_nodes = parse(&arena, "(A)", config, false).unwrap();
let rhs_nodes = parse(&arena, "(B)", config, false).unwrap();
Expand Down Expand Up @@ -700,7 +712,9 @@ mod tests {
fn test_split_unchanged_middle() {
let arena = Arena::new();
let params = Params::default();
let config = params.language(guess_language::Language::EmacsLisp);
let config = params
.language(guess_language::Language::EmacsLisp)
.unwrap();

let lhs_nodes = parse(
&arena,
Expand Down Expand Up @@ -742,7 +756,9 @@ mod tests {
fn test_split_unchanged_multiple() {
let arena = Arena::new();
let params = Params::default();
let config = params.language(guess_language::Language::EmacsLisp);
let config = params
.language(guess_language::Language::EmacsLisp)
.unwrap();

let lhs_nodes = parse(
&arena,
Expand Down Expand Up @@ -771,7 +787,9 @@ mod tests {
fn test_split_unchanged_outer_delimiter() {
let arena = Arena::new();
let params = Params::default();
let config = params.language(guess_language::Language::EmacsLisp);
let config = params
.language(guess_language::Language::EmacsLisp)
.unwrap();

let lhs_nodes = parse(
&arena,
Expand Down Expand Up @@ -803,7 +821,9 @@ mod tests {
fn test_split_mostly_unchanged_toplevel() {
let arena = Arena::new();
let params = Params::default();
let config = params.language(guess_language::Language::EmacsLisp);
let config = params
.language(guess_language::Language::EmacsLisp)
.unwrap();

let lhs_nodes = parse(
&arena,
Expand All @@ -829,7 +849,9 @@ mod tests {
fn test_count_common_unique() {
let arena = Arena::new();
let params = Params::default();
let config = params.language(guess_language::Language::EmacsLisp);
let config = params
.language(guess_language::Language::EmacsLisp)
.unwrap();

// There are two subtrees that are unique on both sides and
// shared between the two sides here:
Expand Down Expand Up @@ -859,7 +881,9 @@ mod tests {
fn test_similar_with_common_grandchildren() {
let arena = Arena::new();
let params = Params::default();
let config = params.language(guess_language::Language::EmacsLisp);
let config = params
.language(guess_language::Language::EmacsLisp)
.unwrap();

let lhs_nodes = parse(&arena, "((novel-lhs 1 2 3 4 5)) x", config, false).unwrap();
let rhs_nodes = parse(&arena, "((novel-rhs 1 2 3 4 5)) y", config, false).unwrap();
Expand All @@ -874,7 +898,9 @@ mod tests {
fn test_similar_ignore_delimiter() {
let arena = Arena::new();
let params = Params::default();
let config = params.language(guess_language::Language::EmacsLisp);
let config = params
.language(guess_language::Language::EmacsLisp)
.unwrap();

let lhs_nodes = parse(&arena, "(novel-lhs 1 2 3 4 5) x", config, false).unwrap();
let rhs_nodes = parse(&arena, "[novel-rhs 1 2 3 4 5] y", config, false).unwrap();
Expand Down
Loading
Loading