diff --git a/cli/examples/custom-commit-templater/main.rs b/cli/examples/custom-commit-templater/main.rs index 03a410b88..ac90684c5 100644 --- a/cli/examples/custom-commit-templater/main.rs +++ b/cli/examples/custom-commit-templater/main.rs @@ -122,7 +122,7 @@ impl CommitTemplateLanguageExtension for HexCounter { let mut table = CommitTemplateBuildFnTable::empty(); table.commit_methods.insert( "has_most_digits", - |language, _build_context, property, call| { + |language, _diagnostics, _build_context, property, call| { call.expect_no_arguments()?; let most_digits = language .cache_extension::() @@ -135,7 +135,7 @@ impl CommitTemplateLanguageExtension for HexCounter { ); table.commit_methods.insert( "num_digits_in_id", - |_language, _build_context, property, call| { + |_language, _diagnostics, _build_context, property, call| { call.expect_no_arguments()?; Ok(L::wrap_integer( property.map(|commit| num_digits_in_id(commit.id())), @@ -144,7 +144,7 @@ impl CommitTemplateLanguageExtension for HexCounter { ); table.commit_methods.insert( "num_char_in_id", - |_language, _build_context, property, call| { + |_language, _diagnostics, _build_context, property, call| { let [string_arg] = call.expect_exact_arguments()?; let char_arg = template_parser::expect_string_literal_with(string_arg, |string, span| { diff --git a/cli/examples/custom-operation-templater/main.rs b/cli/examples/custom-operation-templater/main.rs index 09a77c9af..bfd7b0a97 100644 --- a/cli/examples/custom-operation-templater/main.rs +++ b/cli/examples/custom-operation-templater/main.rs @@ -53,7 +53,7 @@ impl OperationTemplateLanguageExtension for HexCounter { let mut table = OperationTemplateBuildFnTable::empty(); table.operation_methods.insert( "num_digits_in_id", - |_language, _build_context, property, call| { + |_language, _diagnostics, _build_context, property, call| { call.expect_no_arguments()?; Ok(L::wrap_integer( property.map(|operation| num_digits_in_id(operation.id())), @@ -62,7 +62,7 @@ impl OperationTemplateLanguageExtension for HexCounter { ); table.operation_methods.insert( "num_char_in_id", - |_language, _build_context, property, call| { + |_language, _diagnostics, _build_context, property, call| { let [string_arg] = call.expect_exact_arguments()?; let char_arg = template_parser::expect_string_literal_with(string_arg, |string, span| { diff --git a/cli/src/cli_util.rs b/cli/src/cli_util.rs index acd083078..c49af6b77 100644 --- a/cli/src/cli_util.rs +++ b/cli/src/cli_util.rs @@ -166,7 +166,7 @@ use crate::revset_util::RevsetExpressionEvaluator; use crate::template_builder; use crate::template_builder::TemplateLanguage; use crate::template_parser::TemplateAliasesMap; -use crate::template_parser::TemplateParseResult; +use crate::template_parser::TemplateDiagnostics; use crate::templater::PropertyPlaceholder; use crate::templater::TemplateRenderer; use crate::text_util; @@ -343,13 +343,17 @@ impl CommandHelper { template_text: &str, wrap_self: impl Fn(PropertyPlaceholder) -> L::Property, ) -> Result, CommandError> { + let mut diagnostics = TemplateDiagnostics::new(); let aliases = self.load_template_aliases(ui)?; - Ok(template_builder::parse( + let template = template_builder::parse( language, + &mut diagnostics, template_text, &aliases, wrap_self, - )?) + )?; + print_parse_diagnostics(ui, "In template expression", &diagnostics)?; + Ok(template) } pub fn workspace_loader(&self) -> Result<&dyn WorkspaceLoader, CommandError> { @@ -780,13 +784,21 @@ impl WorkspaceCommandEnvironment { /// be one of the `L::wrap_*()` functions. pub fn parse_template<'a, C: Clone + 'a, L: TemplateLanguage<'a> + ?Sized>( &self, - _ui: &Ui, + ui: &Ui, language: &L, template_text: &str, wrap_self: impl Fn(PropertyPlaceholder) -> L::Property, - ) -> TemplateParseResult> { - let aliases = &self.template_aliases_map; - template_builder::parse(language, template_text, aliases, wrap_self) + ) -> Result, CommandError> { + let mut diagnostics = TemplateDiagnostics::new(); + let template = template_builder::parse( + language, + &mut diagnostics, + template_text, + &self.template_aliases_map, + wrap_self, + )?; + print_parse_diagnostics(ui, "In template expression", &diagnostics)?; + Ok(template) } /// Creates commit template language environment for this workspace and the @@ -1392,7 +1404,7 @@ impl WorkspaceCommandHelper { language: &L, template_text: &str, wrap_self: impl Fn(PropertyPlaceholder) -> L::Property, - ) -> TemplateParseResult> { + ) -> Result, CommandError> { self.env .parse_template(ui, language, template_text, wrap_self) } @@ -1404,9 +1416,14 @@ impl WorkspaceCommandHelper { template_text: &str, wrap_self: impl Fn(PropertyPlaceholder) -> L::Property, ) -> TemplateRenderer<'a, C> { - let aliases = &self.env.template_aliases_map; - template_builder::parse(language, template_text, aliases, wrap_self) - .expect("parse error should be confined by WorkspaceCommandHelper::new()") + template_builder::parse( + language, + &mut TemplateDiagnostics::new(), + template_text, + &self.env.template_aliases_map, + wrap_self, + ) + .expect("parse error should be confined by WorkspaceCommandHelper::new()") } /// Parses commit template into evaluation tree. @@ -1414,7 +1431,7 @@ impl WorkspaceCommandHelper { &self, ui: &Ui, template_text: &str, - ) -> TemplateParseResult> { + ) -> Result, CommandError> { let language = self.commit_template_language(); self.parse_template( ui, @@ -1429,7 +1446,7 @@ impl WorkspaceCommandHelper { &self, ui: &Ui, template_text: &str, - ) -> TemplateParseResult> { + ) -> Result, CommandError> { let language = self.operation_template_language(); self.parse_template( ui, @@ -2056,7 +2073,7 @@ impl WorkspaceCommandTransaction<'_> { &self, ui: &Ui, template_text: &str, - ) -> TemplateParseResult> { + ) -> Result, CommandError> { let language = self.commit_template_language(); self.helper.env.parse_template( ui, diff --git a/cli/src/commit_templater.rs b/cli/src/commit_templater.rs index 99896a0e9..7c7bc2cea 100644 --- a/cli/src/commit_templater.rs +++ b/cli/src/commit_templater.rs @@ -64,6 +64,7 @@ use crate::template_builder::TemplateLanguage; use crate::template_parser; use crate::template_parser::ExpressionNode; use crate::template_parser::FunctionCallNode; +use crate::template_parser::TemplateDiagnostics; use crate::template_parser::TemplateParseError; use crate::template_parser::TemplateParseResult; use crate::templater; @@ -142,15 +143,17 @@ impl<'repo> TemplateLanguage<'repo> for CommitTemplateLanguage<'repo> { fn build_function( &self, + diagnostics: &mut TemplateDiagnostics, build_ctx: &BuildContext, function: &FunctionCallNode, ) -> TemplateParseResult { let table = &self.build_fn_table.core; - table.build_function(self, build_ctx, function) + table.build_function(self, diagnostics, build_ctx, function) } fn build_method( &self, + diagnostics: &mut TemplateDiagnostics, build_ctx: &BuildContext, property: Self::Property, function: &FunctionCallNode, @@ -159,24 +162,31 @@ impl<'repo> TemplateLanguage<'repo> for CommitTemplateLanguage<'repo> { match property { CommitTemplatePropertyKind::Core(property) => { let table = &self.build_fn_table.core; - table.build_method(self, build_ctx, property, function) + table.build_method(self, diagnostics, build_ctx, property, function) } CommitTemplatePropertyKind::Commit(property) => { let table = &self.build_fn_table.commit_methods; let build = template_parser::lookup_method(type_name, table, function)?; - build(self, build_ctx, property, function) + build(self, diagnostics, build_ctx, property, function) } CommitTemplatePropertyKind::CommitOpt(property) => { let type_name = "Commit"; let table = &self.build_fn_table.commit_methods; let build = template_parser::lookup_method(type_name, table, function)?; let inner_property = property.try_unwrap(type_name); - build(self, build_ctx, Box::new(inner_property), function) + build( + self, + diagnostics, + build_ctx, + Box::new(inner_property), + function, + ) } CommitTemplatePropertyKind::CommitList(property) => { // TODO: migrate to table? template_builder::build_unformattable_list_method( self, + diagnostics, build_ctx, property, function, @@ -186,19 +196,26 @@ impl<'repo> TemplateLanguage<'repo> for CommitTemplateLanguage<'repo> { CommitTemplatePropertyKind::RefName(property) => { let table = &self.build_fn_table.ref_name_methods; let build = template_parser::lookup_method(type_name, table, function)?; - build(self, build_ctx, property, function) + build(self, diagnostics, build_ctx, property, function) } CommitTemplatePropertyKind::RefNameOpt(property) => { let type_name = "RefName"; let table = &self.build_fn_table.ref_name_methods; let build = template_parser::lookup_method(type_name, table, function)?; let inner_property = property.try_unwrap(type_name); - build(self, build_ctx, Box::new(inner_property), function) + build( + self, + diagnostics, + build_ctx, + Box::new(inner_property), + function, + ) } CommitTemplatePropertyKind::RefNameList(property) => { // TODO: migrate to table? template_builder::build_formattable_list_method( self, + diagnostics, build_ctx, property, function, @@ -208,17 +225,17 @@ impl<'repo> TemplateLanguage<'repo> for CommitTemplateLanguage<'repo> { CommitTemplatePropertyKind::CommitOrChangeId(property) => { let table = &self.build_fn_table.commit_or_change_id_methods; let build = template_parser::lookup_method(type_name, table, function)?; - build(self, build_ctx, property, function) + build(self, diagnostics, build_ctx, property, function) } CommitTemplatePropertyKind::ShortestIdPrefix(property) => { let table = &self.build_fn_table.shortest_id_prefix_methods; let build = template_parser::lookup_method(type_name, table, function)?; - build(self, build_ctx, property, function) + build(self, diagnostics, build_ctx, property, function) } CommitTemplatePropertyKind::TreeDiff(property) => { let table = &self.build_fn_table.tree_diff_methods; let build = template_parser::lookup_method(type_name, table, function)?; - build(self, build_ctx, property, function) + build(self, diagnostics, build_ctx, property, function) } } } @@ -500,7 +517,7 @@ fn builtin_commit_methods<'repo>() -> CommitTemplateBuildMethodFnMap<'repo, Comm let mut map = CommitTemplateBuildMethodFnMap::::new(); map.insert( "description", - |_language, _build_ctx, self_property, function| { + |_language, _diagnostics, _build_ctx, self_property, function| { function.expect_no_arguments()?; let out_property = self_property.map(|commit| text_util::complete_newline(commit.description())); @@ -509,7 +526,7 @@ fn builtin_commit_methods<'repo>() -> CommitTemplateBuildMethodFnMap<'repo, Comm ); map.insert( "change_id", - |_language, _build_ctx, self_property, function| { + |_language, _diagnostics, _build_ctx, self_property, function| { function.expect_no_arguments()?; let out_property = self_property.map(|commit| CommitOrChangeId::Change(commit.change_id().to_owned())); @@ -518,7 +535,7 @@ fn builtin_commit_methods<'repo>() -> CommitTemplateBuildMethodFnMap<'repo, Comm ); map.insert( "commit_id", - |_language, _build_ctx, self_property, function| { + |_language, _diagnostics, _build_ctx, self_property, function| { function.expect_no_arguments()?; let out_property = self_property.map(|commit| CommitOrChangeId::Commit(commit.id().to_owned())); @@ -527,7 +544,7 @@ fn builtin_commit_methods<'repo>() -> CommitTemplateBuildMethodFnMap<'repo, Comm ); map.insert( "parents", - |_language, _build_ctx, self_property, function| { + |_language, _diagnostics, _build_ctx, self_property, function| { function.expect_no_arguments()?; let out_property = self_property.and_then(|commit| Ok(commit.parents().try_collect()?)); @@ -536,7 +553,7 @@ fn builtin_commit_methods<'repo>() -> CommitTemplateBuildMethodFnMap<'repo, Comm ); map.insert( "author", - |_language, _build_ctx, self_property, function| { + |_language, _diagnostics, _build_ctx, self_property, function| { function.expect_no_arguments()?; let out_property = self_property.map(|commit| commit.author().clone()); Ok(L::wrap_signature(out_property)) @@ -544,21 +561,24 @@ fn builtin_commit_methods<'repo>() -> CommitTemplateBuildMethodFnMap<'repo, Comm ); map.insert( "committer", - |_language, _build_ctx, self_property, function| { + |_language, _diagnostics, _build_ctx, self_property, function| { function.expect_no_arguments()?; let out_property = self_property.map(|commit| commit.committer().clone()); Ok(L::wrap_signature(out_property)) }, ); - map.insert("mine", |language, _build_ctx, self_property, function| { - function.expect_no_arguments()?; - let user_email = language.revset_parse_context.user_email().to_owned(); - let out_property = self_property.map(move |commit| commit.author().email == user_email); - Ok(L::wrap_boolean(out_property)) - }); + map.insert( + "mine", + |language, _diagnostics, _build_ctx, self_property, function| { + function.expect_no_arguments()?; + let user_email = language.revset_parse_context.user_email().to_owned(); + let out_property = self_property.map(move |commit| commit.author().email == user_email); + Ok(L::wrap_boolean(out_property)) + }, + ); map.insert( "working_copies", - |language, _build_ctx, self_property, function| { + |language, _diagnostics, _build_ctx, self_property, function| { function.expect_no_arguments()?; let repo = language.repo; let out_property = self_property.map(|commit| extract_working_copies(repo, &commit)); @@ -567,7 +587,7 @@ fn builtin_commit_methods<'repo>() -> CommitTemplateBuildMethodFnMap<'repo, Comm ); map.insert( "current_working_copy", - |language, _build_ctx, self_property, function| { + |language, _diagnostics, _build_ctx, self_property, function| { function.expect_no_arguments()?; let repo = language.repo; let workspace_id = language.workspace_id.clone(); @@ -579,7 +599,7 @@ fn builtin_commit_methods<'repo>() -> CommitTemplateBuildMethodFnMap<'repo, Comm ); map.insert( "bookmarks", - |language, _build_ctx, self_property, function| { + |language, _diagnostics, _build_ctx, self_property, function| { function.expect_no_arguments()?; let index = language .keyword_cache @@ -598,7 +618,7 @@ fn builtin_commit_methods<'repo>() -> CommitTemplateBuildMethodFnMap<'repo, Comm ); map.insert( "local_bookmarks", - |language, _build_ctx, self_property, function| { + |language, _diagnostics, _build_ctx, self_property, function| { function.expect_no_arguments()?; let index = language .keyword_cache @@ -617,7 +637,7 @@ fn builtin_commit_methods<'repo>() -> CommitTemplateBuildMethodFnMap<'repo, Comm ); map.insert( "remote_bookmarks", - |language, _build_ctx, self_property, function| { + |language, _diagnostics, _build_ctx, self_property, function| { function.expect_no_arguments()?; let index = language .keyword_cache @@ -639,15 +659,18 @@ fn builtin_commit_methods<'repo>() -> CommitTemplateBuildMethodFnMap<'repo, Comm map.insert("local_branches", map["local_bookmarks"]); map.insert("remote_branches", map["remote_bookmarks"]); - map.insert("tags", |language, _build_ctx, self_property, function| { - function.expect_no_arguments()?; - let index = language.keyword_cache.tags_index(language.repo).clone(); - let out_property = self_property.map(move |commit| index.get(commit.id()).to_vec()); - Ok(L::wrap_ref_name_list(out_property)) - }); + map.insert( + "tags", + |language, _diagnostics, _build_ctx, self_property, function| { + function.expect_no_arguments()?; + let index = language.keyword_cache.tags_index(language.repo).clone(); + let out_property = self_property.map(move |commit| index.get(commit.id()).to_vec()); + Ok(L::wrap_ref_name_list(out_property)) + }, + ); map.insert( "git_refs", - |language, _build_ctx, self_property, function| { + |language, _diagnostics, _build_ctx, self_property, function| { function.expect_no_arguments()?; let index = language.keyword_cache.git_refs_index(language.repo).clone(); let out_property = self_property.map(move |commit| index.get(commit.id()).to_vec()); @@ -656,7 +679,7 @@ fn builtin_commit_methods<'repo>() -> CommitTemplateBuildMethodFnMap<'repo, Comm ); map.insert( "git_head", - |language, _build_ctx, self_property, function| { + |language, _diagnostics, _build_ctx, self_property, function| { function.expect_no_arguments()?; let repo = language.repo; let out_property = self_property.map(|commit| extract_git_head(repo, &commit)); @@ -665,7 +688,7 @@ fn builtin_commit_methods<'repo>() -> CommitTemplateBuildMethodFnMap<'repo, Comm ); map.insert( "divergent", - |language, _build_ctx, self_property, function| { + |language, _diagnostics, _build_ctx, self_property, function| { function.expect_no_arguments()?; let repo = language.repo; let out_property = self_property.map(|commit| { @@ -676,18 +699,21 @@ fn builtin_commit_methods<'repo>() -> CommitTemplateBuildMethodFnMap<'repo, Comm Ok(L::wrap_boolean(out_property)) }, ); - map.insert("hidden", |language, _build_ctx, self_property, function| { - function.expect_no_arguments()?; - let repo = language.repo; - let out_property = self_property.map(|commit| { - let maybe_entries = repo.resolve_change_id(commit.change_id()); - maybe_entries.map_or(true, |entries| !entries.contains(commit.id())) - }); - Ok(L::wrap_boolean(out_property)) - }); + map.insert( + "hidden", + |language, _diagnostics, _build_ctx, self_property, function| { + function.expect_no_arguments()?; + let repo = language.repo; + let out_property = self_property.map(|commit| { + let maybe_entries = repo.resolve_change_id(commit.change_id()); + maybe_entries.map_or(true, |entries| !entries.contains(commit.id())) + }); + Ok(L::wrap_boolean(out_property)) + }, + ); map.insert( "immutable", - |language, _build_ctx, self_property, function| { + |language, _diagnostics, _build_ctx, self_property, function| { function.expect_no_arguments()?; let is_immutable = language .keyword_cache @@ -699,12 +725,12 @@ fn builtin_commit_methods<'repo>() -> CommitTemplateBuildMethodFnMap<'repo, Comm ); map.insert( "contained_in", - |language, _build_ctx, self_property, function| { + |language, diagnostics, _build_ctx, self_property, function| { let [revset_node] = function.expect_exact_arguments()?; let is_contained = template_parser::expect_string_literal_with(revset_node, |revset, span| { - Ok(evaluate_user_revset(language, span, revset)?.containing_fn()) + Ok(evaluate_user_revset(language, diagnostics, span, revset)?.containing_fn()) })?; let out_property = self_property.map(move |commit| is_contained(commit.id())); @@ -713,39 +739,49 @@ fn builtin_commit_methods<'repo>() -> CommitTemplateBuildMethodFnMap<'repo, Comm ); map.insert( "conflict", - |_language, _build_ctx, self_property, function| { + |_language, _diagnostics, _build_ctx, self_property, function| { function.expect_no_arguments()?; let out_property = self_property.and_then(|commit| Ok(commit.has_conflict()?)); Ok(L::wrap_boolean(out_property)) }, ); - map.insert("empty", |language, _build_ctx, self_property, function| { - function.expect_no_arguments()?; - let repo = language.repo; - let out_property = self_property.and_then(|commit| Ok(commit.is_empty(repo)?)); - Ok(L::wrap_boolean(out_property)) - }); - map.insert("diff", |language, _build_ctx, self_property, function| { - let ([], [files_node]) = function.expect_arguments()?; - let files = if let Some(node) = files_node { - expect_fileset_literal(node, language.path_converter)? - } else { - // TODO: defaults to CLI path arguments? - // https://github.com/martinvonz/jj/issues/2933#issuecomment-1925870731 - FilesetExpression::all() - }; - let repo = language.repo; - let matcher: Rc = files.to_matcher().into(); - let out_property = self_property - .and_then(move |commit| Ok(TreeDiff::from_commit(repo, &commit, matcher.clone())?)); - Ok(L::wrap_tree_diff(out_property)) - }); - map.insert("root", |language, _build_ctx, self_property, function| { - function.expect_no_arguments()?; - let repo = language.repo; - let out_property = self_property.map(|commit| commit.id() == repo.store().root_commit_id()); - Ok(L::wrap_boolean(out_property)) - }); + map.insert( + "empty", + |language, _diagnostics, _build_ctx, self_property, function| { + function.expect_no_arguments()?; + let repo = language.repo; + let out_property = self_property.and_then(|commit| Ok(commit.is_empty(repo)?)); + Ok(L::wrap_boolean(out_property)) + }, + ); + map.insert( + "diff", + |language, diagnostics, _build_ctx, self_property, function| { + let ([], [files_node]) = function.expect_arguments()?; + let files = if let Some(node) = files_node { + expect_fileset_literal(diagnostics, node, language.path_converter)? + } else { + // TODO: defaults to CLI path arguments? + // https://github.com/martinvonz/jj/issues/2933#issuecomment-1925870731 + FilesetExpression::all() + }; + let repo = language.repo; + let matcher: Rc = files.to_matcher().into(); + let out_property = self_property + .and_then(move |commit| Ok(TreeDiff::from_commit(repo, &commit, matcher.clone())?)); + Ok(L::wrap_tree_diff(out_property)) + }, + ); + map.insert( + "root", + |language, _diagnostics, _build_ctx, self_property, function| { + function.expect_no_arguments()?; + let repo = language.repo; + let out_property = + self_property.map(|commit| commit.id() == repo.store().root_commit_id()); + Ok(L::wrap_boolean(out_property)) + }, + ); map } @@ -765,14 +801,20 @@ fn extract_working_copies(repo: &dyn Repo, commit: &Commit) -> String { } fn expect_fileset_literal( + diagnostics: &mut TemplateDiagnostics, node: &ExpressionNode, path_converter: &RepoPathUiConverter, ) -> Result { template_parser::expect_string_literal_with(node, |text, span| { - let mut inner_diagnostics = FilesetDiagnostics::new(); // TODO - fileset::parse(&mut inner_diagnostics, text, path_converter).map_err(|err| { - TemplateParseError::expression("In fileset expression", span).with_source(err) - }) + let mut inner_diagnostics = FilesetDiagnostics::new(); + let expression = + fileset::parse(&mut inner_diagnostics, text, path_converter).map_err(|err| { + TemplateParseError::expression("In fileset expression", span).with_source(err) + })?; + diagnostics.extend_with(inner_diagnostics, |diag| { + TemplateParseError::expression("In fileset expression", span).with_source(diag) + }); + Ok(expression) }) } @@ -797,16 +839,20 @@ fn evaluate_revset_expression<'repo>( fn evaluate_user_revset<'repo>( language: &CommitTemplateLanguage<'repo>, + diagnostics: &mut TemplateDiagnostics, span: pest::Span<'_>, revset: &str, ) -> Result, TemplateParseError> { - let mut inner_diagnostics = RevsetDiagnostics::new(); // TODO + let mut inner_diagnostics = RevsetDiagnostics::new(); let (expression, modifier) = revset::parse_with_modifier( &mut inner_diagnostics, revset, &language.revset_parse_context, ) .map_err(|err| TemplateParseError::expression("In revset expression", span).with_source(err))?; + diagnostics.extend_with(inner_diagnostics, |diag| { + TemplateParseError::expression("In revset expression", span).with_source(diag) + }); let (None | Some(RevsetModifier::All)) = modifier; evaluate_revset_expression(language, span, expression) @@ -1003,14 +1049,17 @@ fn builtin_ref_name_methods<'repo>() -> CommitTemplateBuildMethodFnMap<'repo, Rc // Not using maplit::hashmap!{} or custom declarative macro here because // code completion inside macro is quite restricted. let mut map = CommitTemplateBuildMethodFnMap::>::new(); - map.insert("name", |_language, _build_ctx, self_property, function| { - function.expect_no_arguments()?; - let out_property = self_property.map(|ref_name| ref_name.name.clone()); - Ok(L::wrap_string(out_property)) - }); + map.insert( + "name", + |_language, _diagnostics, _build_ctx, self_property, function| { + function.expect_no_arguments()?; + let out_property = self_property.map(|ref_name| ref_name.name.clone()); + Ok(L::wrap_string(out_property)) + }, + ); map.insert( "remote", - |_language, _build_ctx, self_property, function| { + |_language, _diagnostics, _build_ctx, self_property, function| { function.expect_no_arguments()?; let out_property = self_property.map(|ref_name| ref_name.remote.clone().unwrap_or_default()); @@ -1019,7 +1068,7 @@ fn builtin_ref_name_methods<'repo>() -> CommitTemplateBuildMethodFnMap<'repo, Rc ); map.insert( "present", - |_language, _build_ctx, self_property, function| { + |_language, _diagnostics, _build_ctx, self_property, function| { function.expect_no_arguments()?; let out_property = self_property.map(|ref_name| ref_name.is_present()); Ok(L::wrap_boolean(out_property)) @@ -1027,7 +1076,7 @@ fn builtin_ref_name_methods<'repo>() -> CommitTemplateBuildMethodFnMap<'repo, Rc ); map.insert( "conflict", - |_language, _build_ctx, self_property, function| { + |_language, _diagnostics, _build_ctx, self_property, function| { function.expect_no_arguments()?; let out_property = self_property.map(|ref_name| ref_name.has_conflict()); Ok(L::wrap_boolean(out_property)) @@ -1035,7 +1084,7 @@ fn builtin_ref_name_methods<'repo>() -> CommitTemplateBuildMethodFnMap<'repo, Rc ); map.insert( "normal_target", - |language, _build_ctx, self_property, function| { + |language, _diagnostics, _build_ctx, self_property, function| { function.expect_no_arguments()?; let repo = language.repo; let out_property = self_property.and_then(|ref_name| { @@ -1047,7 +1096,7 @@ fn builtin_ref_name_methods<'repo>() -> CommitTemplateBuildMethodFnMap<'repo, Rc ); map.insert( "removed_targets", - |language, _build_ctx, self_property, function| { + |language, _diagnostics, _build_ctx, self_property, function| { function.expect_no_arguments()?; let repo = language.repo; let out_property = self_property.and_then(|ref_name| { @@ -1059,7 +1108,7 @@ fn builtin_ref_name_methods<'repo>() -> CommitTemplateBuildMethodFnMap<'repo, Rc ); map.insert( "added_targets", - |language, _build_ctx, self_property, function| { + |language, _diagnostics, _build_ctx, self_property, function| { function.expect_no_arguments()?; let repo = language.repo; let out_property = self_property.and_then(|ref_name| { @@ -1071,7 +1120,7 @@ fn builtin_ref_name_methods<'repo>() -> CommitTemplateBuildMethodFnMap<'repo, Rc ); map.insert( "tracked", - |_language, _build_ctx, self_property, function| { + |_language, _diagnostics, _build_ctx, self_property, function| { function.expect_no_arguments()?; let out_property = self_property.map(|ref_name| ref_name.is_tracked()); Ok(L::wrap_boolean(out_property)) @@ -1079,7 +1128,7 @@ fn builtin_ref_name_methods<'repo>() -> CommitTemplateBuildMethodFnMap<'repo, Rc ); map.insert( "tracking_present", - |_language, _build_ctx, self_property, function| { + |_language, _diagnostics, _build_ctx, self_property, function| { function.expect_no_arguments()?; let out_property = self_property.map(|ref_name| ref_name.is_tracking_present()); Ok(L::wrap_boolean(out_property)) @@ -1087,7 +1136,7 @@ fn builtin_ref_name_methods<'repo>() -> CommitTemplateBuildMethodFnMap<'repo, Rc ); map.insert( "tracking_ahead_count", - |language, _build_ctx, self_property, function| { + |language, _diagnostics, _build_ctx, self_property, function| { function.expect_no_arguments()?; let repo = language.repo; let out_property = @@ -1097,7 +1146,7 @@ fn builtin_ref_name_methods<'repo>() -> CommitTemplateBuildMethodFnMap<'repo, Rc ); map.insert( "tracking_behind_count", - |language, _build_ctx, self_property, function| { + |language, _diagnostics, _build_ctx, self_property, function| { function.expect_no_arguments()?; let repo = language.repo; let out_property = @@ -1225,7 +1274,7 @@ fn builtin_commit_or_change_id_methods<'repo>( let mut map = CommitTemplateBuildMethodFnMap::::new(); map.insert( "normal_hex", - |_language, _build_ctx, self_property, function| { + |_language, _diagnostics, _build_ctx, self_property, function| { function.expect_no_arguments()?; Ok(L::wrap_string(self_property.map(|id| { // Note: this is _not_ the same as id.hex() for ChangeId, which @@ -1238,22 +1287,39 @@ fn builtin_commit_or_change_id_methods<'repo>( }))) }, ); - map.insert("short", |language, build_ctx, self_property, function| { - let ([], [len_node]) = function.expect_arguments()?; - let len_property = len_node - .map(|node| template_builder::expect_usize_expression(language, build_ctx, node)) - .transpose()?; - let out_property = - (self_property, len_property).map(|(id, len)| id.short(len.unwrap_or(12))); - Ok(L::wrap_string(out_property)) - }); + map.insert( + "short", + |language, diagnostics, build_ctx, self_property, function| { + let ([], [len_node]) = function.expect_arguments()?; + let len_property = len_node + .map(|node| { + template_builder::expect_usize_expression( + language, + diagnostics, + build_ctx, + node, + ) + }) + .transpose()?; + let out_property = + (self_property, len_property).map(|(id, len)| id.short(len.unwrap_or(12))); + Ok(L::wrap_string(out_property)) + }, + ); map.insert( "shortest", - |language, build_ctx, self_property, function| { + |language, diagnostics, build_ctx, self_property, function| { let id_prefix_context = &language.id_prefix_context; let ([], [len_node]) = function.expect_arguments()?; let len_property = len_node - .map(|node| template_builder::expect_usize_expression(language, build_ctx, node)) + .map(|node| { + template_builder::expect_usize_expression( + language, + diagnostics, + build_ctx, + node, + ) + }) .transpose()?; let out_property = (self_property, len_property) .map(|(id, len)| id.shortest(language.repo, id_prefix_context, len.unwrap_or(0))); @@ -1299,27 +1365,36 @@ fn builtin_shortest_id_prefix_methods<'repo>( let mut map = CommitTemplateBuildMethodFnMap::::new(); map.insert( "prefix", - |_language, _build_ctx, self_property, function| { + |_language, _diagnostics, _build_ctx, self_property, function| { function.expect_no_arguments()?; let out_property = self_property.map(|id| id.prefix); Ok(L::wrap_string(out_property)) }, ); - map.insert("rest", |_language, _build_ctx, self_property, function| { - function.expect_no_arguments()?; - let out_property = self_property.map(|id| id.rest); - Ok(L::wrap_string(out_property)) - }); - map.insert("upper", |_language, _build_ctx, self_property, function| { - function.expect_no_arguments()?; - let out_property = self_property.map(|id| id.to_upper()); - Ok(L::wrap_shortest_id_prefix(out_property)) - }); - map.insert("lower", |_language, _build_ctx, self_property, function| { - function.expect_no_arguments()?; - let out_property = self_property.map(|id| id.to_lower()); - Ok(L::wrap_shortest_id_prefix(out_property)) - }); + map.insert( + "rest", + |_language, _diagnostics, _build_ctx, self_property, function| { + function.expect_no_arguments()?; + let out_property = self_property.map(|id| id.rest); + Ok(L::wrap_string(out_property)) + }, + ); + map.insert( + "upper", + |_language, _diagnostics, _build_ctx, self_property, function| { + function.expect_no_arguments()?; + let out_property = self_property.map(|id| id.to_upper()); + Ok(L::wrap_shortest_id_prefix(out_property)) + }, + ); + map.insert( + "lower", + |_language, _diagnostics, _build_ctx, self_property, function| { + function.expect_no_arguments()?; + let out_property = self_property.map(|id| id.to_lower()); + Ok(L::wrap_shortest_id_prefix(out_property)) + }, + ); map } @@ -1392,10 +1467,17 @@ fn builtin_tree_diff_methods<'repo>() -> CommitTemplateBuildMethodFnMap<'repo, T let mut map = CommitTemplateBuildMethodFnMap::::new(); map.insert( "color_words", - |language, build_ctx, self_property, function| { + |language, diagnostics, build_ctx, self_property, function| { let ([], [context_node]) = function.expect_arguments()?; let context_property = context_node - .map(|node| template_builder::expect_usize_expression(language, build_ctx, node)) + .map(|node| { + template_builder::expect_usize_expression( + language, + diagnostics, + build_ctx, + node, + ) + }) .transpose()?; let path_converter = language.path_converter; let template = (self_property, context_property) @@ -1419,38 +1501,61 @@ fn builtin_tree_diff_methods<'repo>() -> CommitTemplateBuildMethodFnMap<'repo, T Ok(L::wrap_template(template)) }, ); - map.insert("git", |language, build_ctx, self_property, function| { - let ([], [context_node]) = function.expect_arguments()?; - let context_property = context_node - .map(|node| template_builder::expect_usize_expression(language, build_ctx, node)) - .transpose()?; - let template = (self_property, context_property) - .map(|(diff, context)| { - let context = context.unwrap_or(diff_util::DEFAULT_CONTEXT_LINES); - diff.into_formatted(move |formatter, store, tree_diff| { - diff_util::show_git_diff(formatter, store, tree_diff, context) + map.insert( + "git", + |language, diagnostics, build_ctx, self_property, function| { + let ([], [context_node]) = function.expect_arguments()?; + let context_property = context_node + .map(|node| { + template_builder::expect_usize_expression( + language, + diagnostics, + build_ctx, + node, + ) }) - }) - .into_template(); - Ok(L::wrap_template(template)) - }); - map.insert("stat", |language, build_ctx, self_property, function| { - let [width_node] = function.expect_exact_arguments()?; - let width_property = - template_builder::expect_usize_expression(language, build_ctx, width_node)?; - let path_converter = language.path_converter; - let template = (self_property, width_property) - .map(move |(diff, width)| { - diff.into_formatted(move |formatter, store, tree_diff| { - diff_util::show_diff_stat(formatter, store, tree_diff, path_converter, width) + .transpose()?; + let template = (self_property, context_property) + .map(|(diff, context)| { + let context = context.unwrap_or(diff_util::DEFAULT_CONTEXT_LINES); + diff.into_formatted(move |formatter, store, tree_diff| { + diff_util::show_git_diff(formatter, store, tree_diff, context) + }) }) - }) - .into_template(); - Ok(L::wrap_template(template)) - }); + .into_template(); + Ok(L::wrap_template(template)) + }, + ); + map.insert( + "stat", + |language, diagnostics, build_ctx, self_property, function| { + let [width_node] = function.expect_exact_arguments()?; + let width_property = template_builder::expect_usize_expression( + language, + diagnostics, + build_ctx, + width_node, + )?; + let path_converter = language.path_converter; + let template = (self_property, width_property) + .map(move |(diff, width)| { + diff.into_formatted(move |formatter, store, tree_diff| { + diff_util::show_diff_stat( + formatter, + store, + tree_diff, + path_converter, + width, + ) + }) + }) + .into_template(); + Ok(L::wrap_template(template)) + }, + ); map.insert( "summary", - |language, _build_ctx, self_property, function| { + |language, _diagnostics, _build_ctx, self_property, function| { function.expect_no_arguments()?; let path_converter = language.path_converter; let template = self_property diff --git a/cli/src/generic_templater.rs b/cli/src/generic_templater.rs index 3feb9d6f3..fc85181ec 100644 --- a/cli/src/generic_templater.rs +++ b/cli/src/generic_templater.rs @@ -22,6 +22,7 @@ use crate::template_builder::IntoTemplateProperty; use crate::template_builder::TemplateLanguage; use crate::template_parser; use crate::template_parser::FunctionCallNode; +use crate::template_parser::TemplateDiagnostics; use crate::template_parser::TemplateParseResult; use crate::templater::Template; use crate::templater::TemplateProperty; @@ -86,15 +87,17 @@ impl<'a, C: 'a> TemplateLanguage<'a> for GenericTemplateLanguage<'a, C> { fn build_function( &self, + diagnostics: &mut TemplateDiagnostics, build_ctx: &BuildContext, function: &FunctionCallNode, ) -> TemplateParseResult { let table = &self.build_fn_table.core; - table.build_function(self, build_ctx, function) + table.build_function(self, diagnostics, build_ctx, function) } fn build_method( &self, + diagnostics: &mut TemplateDiagnostics, build_ctx: &BuildContext, property: Self::Property, function: &FunctionCallNode, @@ -103,7 +106,7 @@ impl<'a, C: 'a> TemplateLanguage<'a> for GenericTemplateLanguage<'a, C> { match property { GenericTemplatePropertyKind::Core(property) => { let table = &self.build_fn_table.core; - table.build_method(self, build_ctx, property, function) + table.build_method(self, diagnostics, build_ctx, property, function) } GenericTemplatePropertyKind::Self_(property) => { let table = &self.build_fn_table.keywords; diff --git a/cli/src/operation_templater.rs b/cli/src/operation_templater.rs index eb6b8dc3b..7a9ccf0b4 100644 --- a/cli/src/operation_templater.rs +++ b/cli/src/operation_templater.rs @@ -32,6 +32,7 @@ use crate::template_builder::TemplateBuildMethodFnMap; use crate::template_builder::TemplateLanguage; use crate::template_parser; use crate::template_parser::FunctionCallNode; +use crate::template_parser::TemplateDiagnostics; use crate::template_parser::TemplateParseResult; use crate::templater::PlainTextFormattedProperty; use crate::templater::Template; @@ -87,15 +88,17 @@ impl TemplateLanguage<'static> for OperationTemplateLanguage { fn build_function( &self, + diagnostics: &mut TemplateDiagnostics, build_ctx: &BuildContext, function: &FunctionCallNode, ) -> TemplateParseResult { let table = &self.build_fn_table.core; - table.build_function(self, build_ctx, function) + table.build_function(self, diagnostics, build_ctx, function) } fn build_method( &self, + diagnostics: &mut TemplateDiagnostics, build_ctx: &BuildContext, property: Self::Property, function: &FunctionCallNode, @@ -104,17 +107,17 @@ impl TemplateLanguage<'static> for OperationTemplateLanguage { match property { OperationTemplatePropertyKind::Core(property) => { let table = &self.build_fn_table.core; - table.build_method(self, build_ctx, property, function) + table.build_method(self, diagnostics, build_ctx, property, function) } OperationTemplatePropertyKind::Operation(property) => { let table = &self.build_fn_table.operation_methods; let build = template_parser::lookup_method(type_name, table, function)?; - build(self, build_ctx, property, function) + build(self, diagnostics, build_ctx, property, function) } OperationTemplatePropertyKind::OperationId(property) => { let table = &self.build_fn_table.operation_id_methods; let build = template_parser::lookup_method(type_name, table, function)?; - build(self, build_ctx, property, function) + build(self, diagnostics, build_ctx, property, function) } } } @@ -236,7 +239,7 @@ fn builtin_operation_methods() -> OperationTemplateBuildMethodFnMap { let mut map = OperationTemplateBuildMethodFnMap::::new(); map.insert( "current_operation", - |language, _build_ctx, self_property, function| { + |language, _diagnostics, _build_ctx, self_property, function| { function.expect_no_arguments()?; let current_op_id = language.current_op_id.clone(); let out_property = self_property.map(move |op| Some(op.id()) == current_op_id.as_ref()); @@ -245,59 +248,74 @@ fn builtin_operation_methods() -> OperationTemplateBuildMethodFnMap { ); map.insert( "description", - |_language, _build_ctx, self_property, function| { + |_language, _diagnostics, _build_ctx, self_property, function| { function.expect_no_arguments()?; let out_property = self_property.map(|op| op.metadata().description.clone()); Ok(L::wrap_string(out_property)) }, ); - map.insert("id", |_language, _build_ctx, self_property, function| { - function.expect_no_arguments()?; - let out_property = self_property.map(|op| op.id().clone()); - Ok(L::wrap_operation_id(out_property)) - }); - map.insert("tags", |_language, _build_ctx, self_property, function| { - function.expect_no_arguments()?; - let out_property = self_property.map(|op| { - // TODO: introduce map type - op.metadata() - .tags - .iter() - .map(|(key, value)| format!("{key}: {value}")) - .join("\n") - }); - Ok(L::wrap_string(out_property)) - }); + map.insert( + "id", + |_language, _diagnostics, _build_ctx, self_property, function| { + function.expect_no_arguments()?; + let out_property = self_property.map(|op| op.id().clone()); + Ok(L::wrap_operation_id(out_property)) + }, + ); + map.insert( + "tags", + |_language, _diagnostics, _build_ctx, self_property, function| { + function.expect_no_arguments()?; + let out_property = self_property.map(|op| { + // TODO: introduce map type + op.metadata() + .tags + .iter() + .map(|(key, value)| format!("{key}: {value}")) + .join("\n") + }); + Ok(L::wrap_string(out_property)) + }, + ); map.insert( "snapshot", - |_language, _build_ctx, self_property, function| { + |_language, _diagnostics, _build_ctx, self_property, function| { function.expect_no_arguments()?; let out_property = self_property.map(|op| op.metadata().is_snapshot); Ok(L::wrap_boolean(out_property)) }, ); - map.insert("time", |_language, _build_ctx, self_property, function| { - function.expect_no_arguments()?; - let out_property = self_property.map(|op| TimestampRange { - start: op.metadata().start_time, - end: op.metadata().end_time, - }); - Ok(L::wrap_timestamp_range(out_property)) - }); - map.insert("user", |_language, _build_ctx, self_property, function| { - function.expect_no_arguments()?; - let out_property = self_property.map(|op| { - // TODO: introduce dedicated type and provide accessors? - format!("{}@{}", op.metadata().username, op.metadata().hostname) - }); - Ok(L::wrap_string(out_property)) - }); - map.insert("root", |language, _build_ctx, self_property, function| { - function.expect_no_arguments()?; - let root_op_id = language.root_op_id.clone(); - let out_property = self_property.map(move |op| op.id() == &root_op_id); - Ok(L::wrap_boolean(out_property)) - }); + map.insert( + "time", + |_language, _diagnostics, _build_ctx, self_property, function| { + function.expect_no_arguments()?; + let out_property = self_property.map(|op| TimestampRange { + start: op.metadata().start_time, + end: op.metadata().end_time, + }); + Ok(L::wrap_timestamp_range(out_property)) + }, + ); + map.insert( + "user", + |_language, _diagnostics, _build_ctx, self_property, function| { + function.expect_no_arguments()?; + let out_property = self_property.map(|op| { + // TODO: introduce dedicated type and provide accessors? + format!("{}@{}", op.metadata().username, op.metadata().hostname) + }); + Ok(L::wrap_string(out_property)) + }, + ); + map.insert( + "root", + |language, _diagnostics, _build_ctx, self_property, function| { + function.expect_no_arguments()?; + let root_op_id = language.root_op_id.clone(); + let out_property = self_property.map(move |op| op.id() == &root_op_id); + Ok(L::wrap_boolean(out_property)) + }, + ); map } @@ -312,17 +330,27 @@ fn builtin_operation_id_methods() -> OperationTemplateBuildMethodFnMap::new(); - map.insert("short", |language, build_ctx, self_property, function| { - let ([], [len_node]) = function.expect_arguments()?; - let len_property = len_node - .map(|node| template_builder::expect_usize_expression(language, build_ctx, node)) - .transpose()?; - let out_property = (self_property, len_property).map(|(id, len)| { - let mut hex = id.hex(); - hex.truncate(len.unwrap_or(12)); - hex - }); - Ok(L::wrap_string(out_property)) - }); + map.insert( + "short", + |language, diagnostics, build_ctx, self_property, function| { + let ([], [len_node]) = function.expect_arguments()?; + let len_property = len_node + .map(|node| { + template_builder::expect_usize_expression( + language, + diagnostics, + build_ctx, + node, + ) + }) + .transpose()?; + let out_property = (self_property, len_property).map(|(id, len)| { + let mut hex = id.hex(); + hex.truncate(len.unwrap_or(12)); + hex + }); + Ok(L::wrap_string(out_property)) + }, + ); map } diff --git a/cli/src/template_builder.rs b/cli/src/template_builder.rs index 8a16fbc78..bc152fcd2 100644 --- a/cli/src/template_builder.rs +++ b/cli/src/template_builder.rs @@ -25,6 +25,7 @@ use crate::template_parser::ExpressionKind; use crate::template_parser::ExpressionNode; use crate::template_parser::FunctionCallNode; use crate::template_parser::TemplateAliasesMap; +use crate::template_parser::TemplateDiagnostics; use crate::template_parser::TemplateParseError; use crate::template_parser::TemplateParseErrorKind; use crate::template_parser::TemplateParseResult; @@ -79,12 +80,14 @@ pub trait TemplateLanguage<'a> { /// `CoreTemplateBuildFnTable::build_function()`. fn build_function( &self, + diagnostics: &mut TemplateDiagnostics, build_ctx: &BuildContext, function: &FunctionCallNode, ) -> TemplateParseResult; fn build_method( &self, + diagnostics: &mut TemplateDiagnostics, build_ctx: &BuildContext, property: Self::Property, function: &FunctionCallNode, @@ -269,6 +272,7 @@ impl<'a> IntoTemplateProperty<'a> for CoreTemplatePropertyKind<'a> { pub type TemplateBuildFunctionFn<'a, L> = fn( &L, + &mut TemplateDiagnostics, &BuildContext<>::Property>, &FunctionCallNode, ) -> TemplateParseResult<>::Property>; @@ -277,6 +281,7 @@ pub type TemplateBuildFunctionFn<'a, L> = pub type TemplateBuildMethodFn<'a, L, T> = fn( &L, + &mut TemplateDiagnostics, &BuildContext<>::Property>, Box + 'a>, &FunctionCallNode, @@ -363,12 +368,13 @@ impl<'a, L: TemplateLanguage<'a> + ?Sized> CoreTemplateBuildFnTable<'a, L> { pub fn build_function( &self, language: &L, + diagnostics: &mut TemplateDiagnostics, build_ctx: &BuildContext, function: &FunctionCallNode, ) -> TemplateParseResult { let table = &self.functions; let build = template_parser::lookup_function(table, function)?; - build(language, build_ctx, function) + build(language, diagnostics, build_ctx, function) } /// Applies the method call node `function` to the given `property` by using @@ -376,6 +382,7 @@ impl<'a, L: TemplateLanguage<'a> + ?Sized> CoreTemplateBuildFnTable<'a, L> { pub fn build_method( &self, language: &L, + diagnostics: &mut TemplateDiagnostics, build_ctx: &BuildContext, property: CoreTemplatePropertyKind<'a>, function: &FunctionCallNode, @@ -385,50 +392,61 @@ impl<'a, L: TemplateLanguage<'a> + ?Sized> CoreTemplateBuildFnTable<'a, L> { CoreTemplatePropertyKind::String(property) => { let table = &self.string_methods; let build = template_parser::lookup_method(type_name, table, function)?; - build(language, build_ctx, property, function) + build(language, diagnostics, build_ctx, property, function) } CoreTemplatePropertyKind::StringList(property) => { // TODO: migrate to table? - build_formattable_list_method(language, build_ctx, property, function, |item| { - L::wrap_string(item) - }) + build_formattable_list_method( + language, + diagnostics, + build_ctx, + property, + function, + L::wrap_string, + ) } CoreTemplatePropertyKind::Boolean(property) => { let table = &self.boolean_methods; let build = template_parser::lookup_method(type_name, table, function)?; - build(language, build_ctx, property, function) + build(language, diagnostics, build_ctx, property, function) } CoreTemplatePropertyKind::Integer(property) => { let table = &self.integer_methods; let build = template_parser::lookup_method(type_name, table, function)?; - build(language, build_ctx, property, function) + build(language, diagnostics, build_ctx, property, function) } CoreTemplatePropertyKind::IntegerOpt(property) => { let type_name = "Integer"; let table = &self.integer_methods; let build = template_parser::lookup_method(type_name, table, function)?; let inner_property = property.try_unwrap(type_name); - build(language, build_ctx, Box::new(inner_property), function) + build( + language, + diagnostics, + build_ctx, + Box::new(inner_property), + function, + ) } CoreTemplatePropertyKind::Signature(property) => { let table = &self.signature_methods; let build = template_parser::lookup_method(type_name, table, function)?; - build(language, build_ctx, property, function) + build(language, diagnostics, build_ctx, property, function) } CoreTemplatePropertyKind::SizeHint(property) => { let table = &self.size_hint_methods; let build = template_parser::lookup_method(type_name, table, function)?; - build(language, build_ctx, property, function) + build(language, diagnostics, build_ctx, property, function) } CoreTemplatePropertyKind::Timestamp(property) => { let table = &self.timestamp_methods; let build = template_parser::lookup_method(type_name, table, function)?; - build(language, build_ctx, property, function) + build(language, diagnostics, build_ctx, property, function) } CoreTemplatePropertyKind::TimestampRange(property) => { let table = &self.timestamp_range_methods; let build = template_parser::lookup_method(type_name, table, function)?; - build(language, build_ctx, property, function) + build(language, diagnostics, build_ctx, property, function) } CoreTemplatePropertyKind::Template(_) => { // TODO: migrate to table? @@ -436,7 +454,7 @@ impl<'a, L: TemplateLanguage<'a> + ?Sized> CoreTemplateBuildFnTable<'a, L> { } CoreTemplatePropertyKind::ListTemplate(template) => { // TODO: migrate to table? - build_list_template_method(language, build_ctx, template, function) + build_list_template_method(language, diagnostics, build_ctx, template, function) } } } @@ -499,6 +517,7 @@ pub struct BuildContext<'i, P> { fn build_keyword<'a, L: TemplateLanguage<'a> + ?Sized>( language: &L, + diagnostics: &mut TemplateDiagnostics, build_ctx: &BuildContext, name: &str, name_span: pest::Span<'_>, @@ -513,7 +532,7 @@ fn build_keyword<'a, L: TemplateLanguage<'a> + ?Sized>( args_span: name_span.end_pos().span(&name_span.end_pos()), }; language - .build_method(build_ctx, self_property, &function) + .build_method(diagnostics, build_ctx, self_property, &function) .map_err(|err| match err.kind() { TemplateParseErrorKind::NoSuchMethod { candidates, .. } => { let kind = TemplateParseErrorKind::NoSuchKeyword { @@ -540,17 +559,18 @@ fn build_keyword<'a, L: TemplateLanguage<'a> + ?Sized>( fn build_unary_operation<'a, L: TemplateLanguage<'a> + ?Sized>( language: &L, + diagnostics: &mut TemplateDiagnostics, build_ctx: &BuildContext, op: UnaryOp, arg_node: &ExpressionNode, ) -> TemplateParseResult { match op { UnaryOp::LogicalNot => { - let arg = expect_boolean_expression(language, build_ctx, arg_node)?; + let arg = expect_boolean_expression(language, diagnostics, build_ctx, arg_node)?; Ok(L::wrap_boolean(arg.map(|v| !v))) } UnaryOp::Negate => { - let arg = expect_integer_expression(language, build_ctx, arg_node)?; + let arg = expect_integer_expression(language, diagnostics, build_ctx, arg_node)?; Ok(L::wrap_integer(arg.and_then(|v| { v.checked_neg() .ok_or_else(|| TemplatePropertyError("Attempt to negate with overflow".into())) @@ -561,6 +581,7 @@ fn build_unary_operation<'a, L: TemplateLanguage<'a> + ?Sized>( fn build_binary_operation<'a, L: TemplateLanguage<'a> + ?Sized>( language: &L, + diagnostics: &mut TemplateDiagnostics, build_ctx: &BuildContext, op: BinaryOp, lhs_node: &ExpressionNode, @@ -568,14 +589,14 @@ fn build_binary_operation<'a, L: TemplateLanguage<'a> + ?Sized>( ) -> TemplateParseResult { match op { BinaryOp::LogicalOr => { - let lhs = expect_boolean_expression(language, build_ctx, lhs_node)?; - let rhs = expect_boolean_expression(language, build_ctx, rhs_node)?; + let lhs = expect_boolean_expression(language, diagnostics, build_ctx, lhs_node)?; + let rhs = expect_boolean_expression(language, diagnostics, build_ctx, rhs_node)?; let out = lhs.and_then(move |l| Ok(l || rhs.extract()?)); Ok(L::wrap_boolean(out)) } BinaryOp::LogicalAnd => { - let lhs = expect_boolean_expression(language, build_ctx, lhs_node)?; - let rhs = expect_boolean_expression(language, build_ctx, rhs_node)?; + let lhs = expect_boolean_expression(language, diagnostics, build_ctx, lhs_node)?; + let rhs = expect_boolean_expression(language, diagnostics, build_ctx, rhs_node)?; let out = lhs.and_then(move |l| Ok(l && rhs.extract()?)); Ok(L::wrap_boolean(out)) } @@ -587,17 +608,21 @@ fn builtin_string_methods<'a, L: TemplateLanguage<'a> + ?Sized>( // Not using maplit::hashmap!{} or custom declarative macro here because // code completion inside macro is quite restricted. let mut map = TemplateBuildMethodFnMap::::new(); - map.insert("len", |_language, _build_ctx, self_property, function| { - function.expect_no_arguments()?; - let out_property = self_property.and_then(|s| Ok(s.len().try_into()?)); - Ok(L::wrap_integer(out_property)) - }); + map.insert( + "len", + |_language, _diagnostics, _build_ctx, self_property, function| { + function.expect_no_arguments()?; + let out_property = self_property.and_then(|s| Ok(s.len().try_into()?)); + Ok(L::wrap_integer(out_property)) + }, + ); map.insert( "contains", - |language, build_ctx, self_property, function| { + |language, diagnostics, build_ctx, self_property, function| { let [needle_node] = function.expect_exact_arguments()?; // TODO: or .try_into_string() to disable implicit type cast? - let needle_property = expect_plain_text_expression(language, build_ctx, needle_node)?; + let needle_property = + expect_plain_text_expression(language, diagnostics, build_ctx, needle_node)?; let out_property = (self_property, needle_property) .map(|(haystack, needle)| haystack.contains(&needle)); Ok(L::wrap_boolean(out_property)) @@ -605,9 +630,10 @@ fn builtin_string_methods<'a, L: TemplateLanguage<'a> + ?Sized>( ); map.insert( "starts_with", - |language, build_ctx, self_property, function| { + |language, diagnostics, build_ctx, self_property, function| { let [needle_node] = function.expect_exact_arguments()?; - let needle_property = expect_plain_text_expression(language, build_ctx, needle_node)?; + let needle_property = + expect_plain_text_expression(language, diagnostics, build_ctx, needle_node)?; let out_property = (self_property, needle_property) .map(|(haystack, needle)| haystack.starts_with(&needle)); Ok(L::wrap_boolean(out_property)) @@ -615,9 +641,10 @@ fn builtin_string_methods<'a, L: TemplateLanguage<'a> + ?Sized>( ); map.insert( "ends_with", - |language, build_ctx, self_property, function| { + |language, diagnostics, build_ctx, self_property, function| { let [needle_node] = function.expect_exact_arguments()?; - let needle_property = expect_plain_text_expression(language, build_ctx, needle_node)?; + let needle_property = + expect_plain_text_expression(language, diagnostics, build_ctx, needle_node)?; let out_property = (self_property, needle_property) .map(|(haystack, needle)| haystack.ends_with(&needle)); Ok(L::wrap_boolean(out_property)) @@ -625,9 +652,10 @@ fn builtin_string_methods<'a, L: TemplateLanguage<'a> + ?Sized>( ); map.insert( "remove_prefix", - |language, build_ctx, self_property, function| { + |language, diagnostics, build_ctx, self_property, function| { let [needle_node] = function.expect_exact_arguments()?; - let needle_property = expect_plain_text_expression(language, build_ctx, needle_node)?; + let needle_property = + expect_plain_text_expression(language, diagnostics, build_ctx, needle_node)?; let out_property = (self_property, needle_property).map(|(haystack, needle)| { haystack .strip_prefix(&needle) @@ -639,9 +667,10 @@ fn builtin_string_methods<'a, L: TemplateLanguage<'a> + ?Sized>( ); map.insert( "remove_suffix", - |language, build_ctx, self_property, function| { + |language, diagnostics, build_ctx, self_property, function| { let [needle_node] = function.expect_exact_arguments()?; - let needle_property = expect_plain_text_expression(language, build_ctx, needle_node)?; + let needle_property = + expect_plain_text_expression(language, diagnostics, build_ctx, needle_node)?; let out_property = (self_property, needle_property).map(|(haystack, needle)| { haystack .strip_suffix(&needle) @@ -651,42 +680,57 @@ fn builtin_string_methods<'a, L: TemplateLanguage<'a> + ?Sized>( Ok(L::wrap_string(out_property)) }, ); - map.insert("substr", |language, build_ctx, self_property, function| { - let [start_idx, end_idx] = function.expect_exact_arguments()?; - let start_idx_property = expect_isize_expression(language, build_ctx, start_idx)?; - let end_idx_property = expect_isize_expression(language, build_ctx, end_idx)?; - let out_property = - (self_property, start_idx_property, end_idx_property).map(|(s, start_idx, end_idx)| { - let start_idx = string_index_to_char_boundary(&s, start_idx); - let end_idx = string_index_to_char_boundary(&s, end_idx); - s.get(start_idx..end_idx).unwrap_or_default().to_owned() - }); - Ok(L::wrap_string(out_property)) - }); + map.insert( + "substr", + |language, diagnostics, build_ctx, self_property, function| { + let [start_idx, end_idx] = function.expect_exact_arguments()?; + let start_idx_property = + expect_isize_expression(language, diagnostics, build_ctx, start_idx)?; + let end_idx_property = + expect_isize_expression(language, diagnostics, build_ctx, end_idx)?; + let out_property = (self_property, start_idx_property, end_idx_property).map( + |(s, start_idx, end_idx)| { + let start_idx = string_index_to_char_boundary(&s, start_idx); + let end_idx = string_index_to_char_boundary(&s, end_idx); + s.get(start_idx..end_idx).unwrap_or_default().to_owned() + }, + ); + Ok(L::wrap_string(out_property)) + }, + ); map.insert( "first_line", - |_language, _build_ctx, self_property, function| { + |_language, _diagnostics, _build_ctx, self_property, function| { function.expect_no_arguments()?; let out_property = self_property.map(|s| s.lines().next().unwrap_or_default().to_string()); Ok(L::wrap_string(out_property)) }, ); - map.insert("lines", |_language, _build_ctx, self_property, function| { - function.expect_no_arguments()?; - let out_property = self_property.map(|s| s.lines().map(|l| l.to_owned()).collect()); - Ok(L::wrap_string_list(out_property)) - }); - map.insert("upper", |_language, _build_ctx, self_property, function| { - function.expect_no_arguments()?; - let out_property = self_property.map(|s| s.to_uppercase()); - Ok(L::wrap_string(out_property)) - }); - map.insert("lower", |_language, _build_ctx, self_property, function| { - function.expect_no_arguments()?; - let out_property = self_property.map(|s| s.to_lowercase()); - Ok(L::wrap_string(out_property)) - }); + map.insert( + "lines", + |_language, _diagnostics, _build_ctx, self_property, function| { + function.expect_no_arguments()?; + let out_property = self_property.map(|s| s.lines().map(|l| l.to_owned()).collect()); + Ok(L::wrap_string_list(out_property)) + }, + ); + map.insert( + "upper", + |_language, _diagnostics, _build_ctx, self_property, function| { + function.expect_no_arguments()?; + let out_property = self_property.map(|s| s.to_uppercase()); + Ok(L::wrap_string(out_property)) + }, + ); + map.insert( + "lower", + |_language, _diagnostics, _build_ctx, self_property, function| { + function.expect_no_arguments()?; + let out_property = self_property.map(|s| s.to_lowercase()); + Ok(L::wrap_string(out_property)) + }, + ); map } @@ -711,19 +755,25 @@ fn builtin_signature_methods<'a, L: TemplateLanguage<'a> + ?Sized>( // Not using maplit::hashmap!{} or custom declarative macro here because // code completion inside macro is quite restricted. let mut map = TemplateBuildMethodFnMap::::new(); - map.insert("name", |_language, _build_ctx, self_property, function| { - function.expect_no_arguments()?; - let out_property = self_property.map(|signature| signature.name); - Ok(L::wrap_string(out_property)) - }); - map.insert("email", |_language, _build_ctx, self_property, function| { - function.expect_no_arguments()?; - let out_property = self_property.map(|signature| signature.email); - Ok(L::wrap_string(out_property)) - }); + map.insert( + "name", + |_language, _diagnostics, _build_ctx, self_property, function| { + function.expect_no_arguments()?; + let out_property = self_property.map(|signature| signature.name); + Ok(L::wrap_string(out_property)) + }, + ); + map.insert( + "email", + |_language, _diagnostics, _build_ctx, self_property, function| { + function.expect_no_arguments()?; + let out_property = self_property.map(|signature| signature.email); + Ok(L::wrap_string(out_property)) + }, + ); map.insert( "username", - |_language, _build_ctx, self_property, function| { + |_language, _diagnostics, _build_ctx, self_property, function| { function.expect_no_arguments()?; let out_property = self_property.map(|signature| { let (username, _) = text_util::split_email(&signature.email); @@ -734,7 +784,7 @@ fn builtin_signature_methods<'a, L: TemplateLanguage<'a> + ?Sized>( ); map.insert( "timestamp", - |_language, _build_ctx, self_property, function| { + |_language, _diagnostics, _build_ctx, self_property, function| { function.expect_no_arguments()?; let out_property = self_property.map(|signature| signature.timestamp); Ok(L::wrap_timestamp(out_property)) @@ -748,30 +798,42 @@ fn builtin_size_hint_methods<'a, L: TemplateLanguage<'a> + ?Sized>( // Not using maplit::hashmap!{} or custom declarative macro here because // code completion inside macro is quite restricted. let mut map = TemplateBuildMethodFnMap::::new(); - map.insert("lower", |_language, _build_ctx, self_property, function| { - function.expect_no_arguments()?; - let out_property = self_property.and_then(|(lower, _)| Ok(i64::try_from(lower)?)); - Ok(L::wrap_integer(out_property)) - }); - map.insert("upper", |_language, _build_ctx, self_property, function| { - function.expect_no_arguments()?; - let out_property = - self_property.and_then(|(_, upper)| Ok(upper.map(i64::try_from).transpose()?)); - Ok(L::wrap_integer_opt(out_property)) - }); - map.insert("exact", |_language, _build_ctx, self_property, function| { - function.expect_no_arguments()?; - let out_property = self_property.and_then(|(lower, upper)| { - let exact = (Some(lower) == upper).then_some(lower); - Ok(exact.map(i64::try_from).transpose()?) - }); - Ok(L::wrap_integer_opt(out_property)) - }); - map.insert("zero", |_language, _build_ctx, self_property, function| { - function.expect_no_arguments()?; - let out_property = self_property.map(|(_, upper)| upper == Some(0)); - Ok(L::wrap_boolean(out_property)) - }); + map.insert( + "lower", + |_language, _diagnostics, _build_ctx, self_property, function| { + function.expect_no_arguments()?; + let out_property = self_property.and_then(|(lower, _)| Ok(i64::try_from(lower)?)); + Ok(L::wrap_integer(out_property)) + }, + ); + map.insert( + "upper", + |_language, _diagnostics, _build_ctx, self_property, function| { + function.expect_no_arguments()?; + let out_property = + self_property.and_then(|(_, upper)| Ok(upper.map(i64::try_from).transpose()?)); + Ok(L::wrap_integer_opt(out_property)) + }, + ); + map.insert( + "exact", + |_language, _diagnostics, _build_ctx, self_property, function| { + function.expect_no_arguments()?; + let out_property = self_property.and_then(|(lower, upper)| { + let exact = (Some(lower) == upper).then_some(lower); + Ok(exact.map(i64::try_from).transpose()?) + }); + Ok(L::wrap_integer_opt(out_property)) + }, + ); + map.insert( + "zero", + |_language, _diagnostics, _build_ctx, self_property, function| { + function.expect_no_arguments()?; + let out_property = self_property.map(|(_, upper)| upper == Some(0)); + Ok(L::wrap_boolean(out_property)) + }, + ); map } @@ -780,17 +842,21 @@ fn builtin_timestamp_methods<'a, L: TemplateLanguage<'a> + ?Sized>( // Not using maplit::hashmap!{} or custom declarative macro here because // code completion inside macro is quite restricted. let mut map = TemplateBuildMethodFnMap::::new(); - map.insert("ago", |_language, _build_ctx, self_property, function| { - function.expect_no_arguments()?; - let now = Timestamp::now(); - let format = timeago::Formatter::new(); - let out_property = self_property - .and_then(move |timestamp| Ok(time_util::format_duration(×tamp, &now, &format)?)); - Ok(L::wrap_string(out_property)) - }); + map.insert( + "ago", + |_language, _diagnostics, _build_ctx, self_property, function| { + function.expect_no_arguments()?; + let now = Timestamp::now(); + let format = timeago::Formatter::new(); + let out_property = self_property.and_then(move |timestamp| { + Ok(time_util::format_duration(×tamp, &now, &format)?) + }); + Ok(L::wrap_string(out_property)) + }, + ); map.insert( "format", - |_language, _build_ctx, self_property, function| { + |_language, _diagnostics, _build_ctx, self_property, function| { // No dynamic string is allowed as the templater has no runtime error type. let [format_node] = function.expect_exact_arguments()?; let format = @@ -807,26 +873,32 @@ fn builtin_timestamp_methods<'a, L: TemplateLanguage<'a> + ?Sized>( Ok(L::wrap_string(out_property)) }, ); - map.insert("utc", |_language, _build_ctx, self_property, function| { - function.expect_no_arguments()?; - let out_property = self_property.map(|mut timestamp| { - timestamp.tz_offset = 0; - timestamp - }); - Ok(L::wrap_timestamp(out_property)) - }); - map.insert("local", |_language, _build_ctx, self_property, function| { - function.expect_no_arguments()?; - let tz_offset = std::env::var("JJ_TZ_OFFSET_MINS") - .ok() - .and_then(|tz_string| tz_string.parse::().ok()) - .unwrap_or_else(|| chrono::Local::now().offset().local_minus_utc() / 60); - let out_property = self_property.map(move |mut timestamp| { - timestamp.tz_offset = tz_offset; - timestamp - }); - Ok(L::wrap_timestamp(out_property)) - }); + map.insert( + "utc", + |_language, _diagnostics, _build_ctx, self_property, function| { + function.expect_no_arguments()?; + let out_property = self_property.map(|mut timestamp| { + timestamp.tz_offset = 0; + timestamp + }); + Ok(L::wrap_timestamp(out_property)) + }, + ); + map.insert( + "local", + |_language, _diagnostics, _build_ctx, self_property, function| { + function.expect_no_arguments()?; + let tz_offset = std::env::var("JJ_TZ_OFFSET_MINS") + .ok() + .and_then(|tz_string| tz_string.parse::().ok()) + .unwrap_or_else(|| chrono::Local::now().offset().local_minus_utc() / 60); + let out_property = self_property.map(move |mut timestamp| { + timestamp.tz_offset = tz_offset; + timestamp + }); + Ok(L::wrap_timestamp(out_property)) + }, + ); map } @@ -835,19 +907,25 @@ fn builtin_timestamp_range_methods<'a, L: TemplateLanguage<'a> + ?Sized>( // Not using maplit::hashmap!{} or custom declarative macro here because // code completion inside macro is quite restricted. let mut map = TemplateBuildMethodFnMap::::new(); - map.insert("start", |_language, _build_ctx, self_property, function| { - function.expect_no_arguments()?; - let out_property = self_property.map(|time_range| time_range.start); - Ok(L::wrap_timestamp(out_property)) - }); - map.insert("end", |_language, _build_ctx, self_property, function| { - function.expect_no_arguments()?; - let out_property = self_property.map(|time_range| time_range.end); - Ok(L::wrap_timestamp(out_property)) - }); + map.insert( + "start", + |_language, _diagnostics, _build_ctx, self_property, function| { + function.expect_no_arguments()?; + let out_property = self_property.map(|time_range| time_range.start); + Ok(L::wrap_timestamp(out_property)) + }, + ); + map.insert( + "end", + |_language, _diagnostics, _build_ctx, self_property, function| { + function.expect_no_arguments()?; + let out_property = self_property.map(|time_range| time_range.end); + Ok(L::wrap_timestamp(out_property)) + }, + ); map.insert( "duration", - |_language, _build_ctx, self_property, function| { + |_language, _diagnostics, _build_ctx, self_property, function| { function.expect_no_arguments()?; let out_property = self_property.and_then(|time_range| Ok(time_range.duration()?)); Ok(L::wrap_string(out_property)) @@ -858,6 +936,7 @@ fn builtin_timestamp_range_methods<'a, L: TemplateLanguage<'a> + ?Sized>( fn build_list_template_method<'a, L: TemplateLanguage<'a> + ?Sized>( language: &L, + diagnostics: &mut TemplateDiagnostics, build_ctx: &BuildContext, self_template: Box, function: &FunctionCallNode, @@ -865,7 +944,8 @@ fn build_list_template_method<'a, L: TemplateLanguage<'a> + ?Sized>( let property = match function.name { "join" => { let [separator_node] = function.expect_exact_arguments()?; - let separator = expect_template_expression(language, build_ctx, separator_node)?; + let separator = + expect_template_expression(language, diagnostics, build_ctx, separator_node)?; L::wrap_template(self_template.join(separator)) } _ => return Err(TemplateParseError::no_such_method("ListTemplate", function)), @@ -876,6 +956,7 @@ fn build_list_template_method<'a, L: TemplateLanguage<'a> + ?Sized>( /// Builds method call expression for printable list property. pub fn build_formattable_list_method<'a, L, O>( language: &L, + diagnostics: &mut TemplateDiagnostics, build_ctx: &BuildContext, self_property: impl TemplateProperty> + 'a, function: &FunctionCallNode, @@ -895,14 +976,22 @@ where } "join" => { let [separator_node] = function.expect_exact_arguments()?; - let separator = expect_template_expression(language, build_ctx, separator_node)?; + let separator = + expect_template_expression(language, diagnostics, build_ctx, separator_node)?; let template = ListPropertyTemplate::new(self_property, separator, |formatter, item| { item.format(formatter) }); L::wrap_template(Box::new(template)) } - "map" => build_map_operation(language, build_ctx, self_property, function, wrap_item)?, + "map" => build_map_operation( + language, + diagnostics, + build_ctx, + self_property, + function, + wrap_item, + )?, _ => return Err(TemplateParseError::no_such_method("List", function)), }; Ok(property) @@ -910,6 +999,7 @@ where pub fn build_unformattable_list_method<'a, L, O>( language: &L, + diagnostics: &mut TemplateDiagnostics, build_ctx: &BuildContext, self_property: impl TemplateProperty> + 'a, function: &FunctionCallNode, @@ -926,7 +1016,14 @@ where L::wrap_integer(out_property) } // No "join" - "map" => build_map_operation(language, build_ctx, self_property, function, wrap_item)?, + "map" => build_map_operation( + language, + diagnostics, + build_ctx, + self_property, + function, + wrap_item, + )?, _ => return Err(TemplateParseError::no_such_method("List", function)), }; Ok(property) @@ -938,6 +1035,7 @@ where /// `wrap_item()` is the function to wrap a list item of type `O` as a property. fn build_map_operation<'a, L, O, P>( language: &L, + diagnostics: &mut TemplateDiagnostics, build_ctx: &BuildContext, self_property: P, function: &FunctionCallNode, @@ -968,7 +1066,7 @@ where local_variables, self_variable: build_ctx.self_variable, }; - expect_template_expression(language, &inner_build_ctx, &lambda.body) + expect_template_expression(language, diagnostics, &inner_build_ctx, &lambda.body) })?; let list_template = ListPropertyTemplate::new( self_property, @@ -984,10 +1082,10 @@ fn builtin_functions<'a, L: TemplateLanguage<'a> + ?Sized>() -> TemplateBuildFun // Not using maplit::hashmap!{} or custom declarative macro here because // code completion inside macro is quite restricted. let mut map = TemplateBuildFunctionFnMap::::new(); - map.insert("fill", |language, build_ctx, function| { + map.insert("fill", |language, diagnostics, build_ctx, function| { let [width_node, content_node] = function.expect_exact_arguments()?; - let width = expect_usize_expression(language, build_ctx, width_node)?; - let content = expect_template_expression(language, build_ctx, content_node)?; + let width = expect_usize_expression(language, diagnostics, build_ctx, width_node)?; + let content = expect_template_expression(language, diagnostics, build_ctx, content_node)?; let template = ReformatTemplate::new(content, move |formatter, recorded| match width.extract() { Ok(width) => text_util::write_wrapped(formatter.as_mut(), recorded, width), @@ -995,10 +1093,10 @@ fn builtin_functions<'a, L: TemplateLanguage<'a> + ?Sized>() -> TemplateBuildFun }); Ok(L::wrap_template(Box::new(template))) }); - map.insert("indent", |language, build_ctx, function| { + map.insert("indent", |language, diagnostics, build_ctx, function| { let [prefix_node, content_node] = function.expect_exact_arguments()?; - let prefix = expect_template_expression(language, build_ctx, prefix_node)?; - let content = expect_template_expression(language, build_ctx, content_node)?; + let prefix = expect_template_expression(language, diagnostics, build_ctx, prefix_node)?; + let content = expect_template_expression(language, diagnostics, build_ctx, content_node)?; let template = ReformatTemplate::new(content, move |formatter, recorded| { let rewrap = formatter.rewrap_fn(); text_util::write_indented(formatter.as_mut(), recorded, |formatter| { @@ -1007,58 +1105,62 @@ fn builtin_functions<'a, L: TemplateLanguage<'a> + ?Sized>() -> TemplateBuildFun }); Ok(L::wrap_template(Box::new(template))) }); - map.insert("label", |language, build_ctx, function| { + map.insert("label", |language, diagnostics, build_ctx, function| { let [label_node, content_node] = function.expect_exact_arguments()?; - let label_property = expect_plain_text_expression(language, build_ctx, label_node)?; - let content = expect_template_expression(language, build_ctx, content_node)?; + let label_property = + expect_plain_text_expression(language, diagnostics, build_ctx, label_node)?; + let content = expect_template_expression(language, diagnostics, build_ctx, content_node)?; let labels = label_property.map(|s| s.split_whitespace().map(ToString::to_string).collect()); Ok(L::wrap_template(Box::new(LabelTemplate::new( content, labels, )))) }); - map.insert("if", |language, build_ctx, function| { + map.insert("if", |language, diagnostics, build_ctx, function| { let ([condition_node, true_node], [false_node]) = function.expect_arguments()?; - let condition = expect_boolean_expression(language, build_ctx, condition_node)?; - let true_template = expect_template_expression(language, build_ctx, true_node)?; + let condition = + expect_boolean_expression(language, diagnostics, build_ctx, condition_node)?; + let true_template = + expect_template_expression(language, diagnostics, build_ctx, true_node)?; let false_template = false_node - .map(|node| expect_template_expression(language, build_ctx, node)) + .map(|node| expect_template_expression(language, diagnostics, build_ctx, node)) .transpose()?; let template = ConditionalTemplate::new(condition, true_template, false_template); Ok(L::wrap_template(Box::new(template))) }); - map.insert("coalesce", |language, build_ctx, function| { + map.insert("coalesce", |language, diagnostics, build_ctx, function| { let contents = function .args .iter() - .map(|node| expect_template_expression(language, build_ctx, node)) + .map(|node| expect_template_expression(language, diagnostics, build_ctx, node)) .try_collect()?; Ok(L::wrap_template(Box::new(CoalesceTemplate(contents)))) }); - map.insert("concat", |language, build_ctx, function| { + map.insert("concat", |language, diagnostics, build_ctx, function| { let contents = function .args .iter() - .map(|node| expect_template_expression(language, build_ctx, node)) + .map(|node| expect_template_expression(language, diagnostics, build_ctx, node)) .try_collect()?; Ok(L::wrap_template(Box::new(ConcatTemplate(contents)))) }); - map.insert("separate", |language, build_ctx, function| { + map.insert("separate", |language, diagnostics, build_ctx, function| { let ([separator_node], content_nodes) = function.expect_some_arguments()?; - let separator = expect_template_expression(language, build_ctx, separator_node)?; + let separator = + expect_template_expression(language, diagnostics, build_ctx, separator_node)?; let contents = content_nodes .iter() - .map(|node| expect_template_expression(language, build_ctx, node)) + .map(|node| expect_template_expression(language, diagnostics, build_ctx, node)) .try_collect()?; Ok(L::wrap_template(Box::new(SeparateTemplate::new( separator, contents, )))) }); - map.insert("surround", |language, build_ctx, function| { + map.insert("surround", |language, diagnostics, build_ctx, function| { let [prefix_node, suffix_node, content_node] = function.expect_exact_arguments()?; - let prefix = expect_template_expression(language, build_ctx, prefix_node)?; - let suffix = expect_template_expression(language, build_ctx, suffix_node)?; - let content = expect_template_expression(language, build_ctx, content_node)?; + let prefix = expect_template_expression(language, diagnostics, build_ctx, prefix_node)?; + let suffix = expect_template_expression(language, diagnostics, build_ctx, suffix_node)?; + let content = expect_template_expression(language, diagnostics, build_ctx, content_node)?; let template = ReformatTemplate::new(content, move |formatter, recorded| { if recorded.data().is_empty() { return Ok(()); @@ -1076,6 +1178,7 @@ fn builtin_functions<'a, L: TemplateLanguage<'a> + ?Sized>() -> TemplateBuildFun /// Builds intermediate expression tree from AST nodes. pub fn build_expression<'a, L: TemplateLanguage<'a> + ?Sized>( language: &L, + diagnostics: &mut TemplateDiagnostics, build_ctx: &BuildContext, node: &ExpressionNode, ) -> TemplateParseResult> { @@ -1089,8 +1192,8 @@ pub fn build_expression<'a, L: TemplateLanguage<'a> + ?Sized>( let make = build_ctx.self_variable; Ok(Expression::unlabeled(make())) } else { - let property = - build_keyword(language, build_ctx, name, node.span).map_err(|err| { + let property = build_keyword(language, diagnostics, build_ctx, name, node.span) + .map_err(|err| { err.extend_keyword_candidates(itertools::chain( build_ctx.local_variables.keys().copied(), ["self"], @@ -1112,29 +1215,35 @@ pub fn build_expression<'a, L: TemplateLanguage<'a> + ?Sized>( Ok(Expression::unlabeled(property)) } ExpressionKind::Unary(op, arg_node) => { - let property = build_unary_operation(language, build_ctx, *op, arg_node)?; + let property = build_unary_operation(language, diagnostics, build_ctx, *op, arg_node)?; Ok(Expression::unlabeled(property)) } ExpressionKind::Binary(op, lhs_node, rhs_node) => { - let property = build_binary_operation(language, build_ctx, *op, lhs_node, rhs_node)?; + let property = + build_binary_operation(language, diagnostics, build_ctx, *op, lhs_node, rhs_node)?; Ok(Expression::unlabeled(property)) } ExpressionKind::Concat(nodes) => { let templates = nodes .iter() - .map(|node| expect_template_expression(language, build_ctx, node)) + .map(|node| expect_template_expression(language, diagnostics, build_ctx, node)) .try_collect()?; let property = L::wrap_template(Box::new(ConcatTemplate(templates))); Ok(Expression::unlabeled(property)) } ExpressionKind::FunctionCall(function) => { - let property = language.build_function(build_ctx, function)?; + let property = language.build_function(diagnostics, build_ctx, function)?; Ok(Expression::unlabeled(property)) } ExpressionKind::MethodCall(method) => { - let mut expression = build_expression(language, build_ctx, &method.object)?; - expression.property = - language.build_method(build_ctx, expression.property, &method.function)?; + let mut expression = + build_expression(language, diagnostics, build_ctx, &method.object)?; + expression.property = language.build_method( + diagnostics, + build_ctx, + expression.property, + &method.function, + )?; expression.labels.push(method.function.name.to_owned()); Ok(expression) } @@ -1142,8 +1251,15 @@ pub fn build_expression<'a, L: TemplateLanguage<'a> + ?Sized>( "Lambda cannot be defined here", node.span, )), - ExpressionKind::AliasExpanded(id, subst) => build_expression(language, build_ctx, subst) - .map_err(|e| e.within_alias_expansion(*id, node.span)), + ExpressionKind::AliasExpanded(id, subst) => { + let mut inner_diagnostics = TemplateDiagnostics::new(); + let expression = build_expression(language, &mut inner_diagnostics, build_ctx, subst) + .map_err(|e| e.within_alias_expansion(*id, node.span))?; + diagnostics.extend_with(inner_diagnostics, |diag| { + diag.within_alias_expansion(*id, node.span) + }); + Ok(expression) + } } } @@ -1153,6 +1269,7 @@ pub fn build_expression<'a, L: TemplateLanguage<'a> + ?Sized>( /// one of the `L::wrap_*()` functions. pub fn build<'a, C: Clone + 'a, L: TemplateLanguage<'a> + ?Sized>( language: &L, + diagnostics: &mut TemplateDiagnostics, node: &ExpressionNode, // TODO: Generic L: WrapProperty trait might be better. See the // comment in build_formattable_list_method(). @@ -1163,48 +1280,63 @@ pub fn build<'a, C: Clone + 'a, L: TemplateLanguage<'a> + ?Sized>( local_variables: HashMap::new(), self_variable: &|| wrap_self(self_placeholder.clone()), }; - let template = expect_template_expression(language, &build_ctx, node)?; + let template = expect_template_expression(language, diagnostics, &build_ctx, node)?; Ok(TemplateRenderer::new(template, self_placeholder)) } /// Parses text, expands aliases, then builds template evaluation tree. pub fn parse<'a, C: Clone + 'a, L: TemplateLanguage<'a> + ?Sized>( language: &L, + diagnostics: &mut TemplateDiagnostics, template_text: &str, aliases_map: &TemplateAliasesMap, wrap_self: impl Fn(PropertyPlaceholder) -> L::Property, ) -> TemplateParseResult> { let node = template_parser::parse(template_text, aliases_map)?; - build(language, &node, wrap_self).map_err(|err| err.extend_alias_candidates(aliases_map)) + build(language, diagnostics, &node, wrap_self) + .map_err(|err| err.extend_alias_candidates(aliases_map)) } pub fn expect_boolean_expression<'a, L: TemplateLanguage<'a> + ?Sized>( language: &L, + diagnostics: &mut TemplateDiagnostics, build_ctx: &BuildContext, node: &ExpressionNode, ) -> TemplateParseResult + 'a>> { - expect_expression_of_type(language, build_ctx, node, "Boolean", |expression| { - expression.try_into_boolean() - }) + expect_expression_of_type( + language, + diagnostics, + build_ctx, + node, + "Boolean", + |expression| expression.try_into_boolean(), + ) } pub fn expect_integer_expression<'a, L: TemplateLanguage<'a> + ?Sized>( language: &L, + diagnostics: &mut TemplateDiagnostics, build_ctx: &BuildContext, node: &ExpressionNode, ) -> TemplateParseResult + 'a>> { - expect_expression_of_type(language, build_ctx, node, "Integer", |expression| { - expression.try_into_integer() - }) + expect_expression_of_type( + language, + diagnostics, + build_ctx, + node, + "Integer", + |expression| expression.try_into_integer(), + ) } /// If the given expression `node` is of `Integer` type, converts it to `isize`. pub fn expect_isize_expression<'a, L: TemplateLanguage<'a> + ?Sized>( language: &L, + diagnostics: &mut TemplateDiagnostics, build_ctx: &BuildContext, node: &ExpressionNode, ) -> TemplateParseResult + 'a>> { - let i64_property = expect_integer_expression(language, build_ctx, node)?; + let i64_property = expect_integer_expression(language, diagnostics, build_ctx, node)?; let isize_property = i64_property.and_then(|v| Ok(isize::try_from(v)?)); Ok(Box::new(isize_property)) } @@ -1212,48 +1344,74 @@ pub fn expect_isize_expression<'a, L: TemplateLanguage<'a> + ?Sized>( /// If the given expression `node` is of `Integer` type, converts it to `usize`. pub fn expect_usize_expression<'a, L: TemplateLanguage<'a> + ?Sized>( language: &L, + diagnostics: &mut TemplateDiagnostics, build_ctx: &BuildContext, node: &ExpressionNode, ) -> TemplateParseResult + 'a>> { - let i64_property = expect_integer_expression(language, build_ctx, node)?; + let i64_property = expect_integer_expression(language, diagnostics, build_ctx, node)?; let usize_property = i64_property.and_then(|v| Ok(usize::try_from(v)?)); Ok(Box::new(usize_property)) } pub fn expect_plain_text_expression<'a, L: TemplateLanguage<'a> + ?Sized>( language: &L, + diagnostics: &mut TemplateDiagnostics, build_ctx: &BuildContext, node: &ExpressionNode, ) -> TemplateParseResult + 'a>> { // Since any formattable type can be converted to a string property, // the expected type is not a String, but a Template. - expect_expression_of_type(language, build_ctx, node, "Template", |expression| { - expression.try_into_plain_text() - }) + expect_expression_of_type( + language, + diagnostics, + build_ctx, + node, + "Template", + |expression| expression.try_into_plain_text(), + ) } pub fn expect_template_expression<'a, L: TemplateLanguage<'a> + ?Sized>( language: &L, + diagnostics: &mut TemplateDiagnostics, build_ctx: &BuildContext, node: &ExpressionNode, ) -> TemplateParseResult> { - expect_expression_of_type(language, build_ctx, node, "Template", |expression| { - expression.try_into_template() - }) + expect_expression_of_type( + language, + diagnostics, + build_ctx, + node, + "Template", + |expression| expression.try_into_template(), + ) } fn expect_expression_of_type<'a, L: TemplateLanguage<'a> + ?Sized, T>( language: &L, + diagnostics: &mut TemplateDiagnostics, build_ctx: &BuildContext, node: &ExpressionNode, expected_type: &str, f: impl FnOnce(Expression) -> Option, ) -> TemplateParseResult { if let ExpressionKind::AliasExpanded(id, subst) = &node.kind { - expect_expression_of_type(language, build_ctx, subst, expected_type, f) - .map_err(|e| e.within_alias_expansion(*id, node.span)) + let mut inner_diagnostics = TemplateDiagnostics::new(); + let expression = expect_expression_of_type( + language, + &mut inner_diagnostics, + build_ctx, + subst, + expected_type, + f, + ) + .map_err(|e| e.within_alias_expansion(*id, node.span))?; + diagnostics.extend_with(inner_diagnostics, |diag| { + diag.within_alias_expansion(*id, node.span) + }); + Ok(expression) } else { - let expression = build_expression(language, build_ctx, node)?; + let expression = build_expression(language, diagnostics, build_ctx, node)?; let actual_type = expression.type_name(); f(expression) .ok_or_else(|| TemplateParseError::expected_type(expected_type, actual_type, node.span)) @@ -1313,7 +1471,13 @@ mod tests { } fn parse(&self, template: &str) -> TemplateParseResult> { - parse(&self.language, template, &self.aliases_map, L::wrap_self) + parse( + &self.language, + &mut TemplateDiagnostics::new(), + template, + &self.aliases_map, + L::wrap_self, + ) } fn parse_err(&self, template: &str) -> String { diff --git a/cli/src/template_parser.rs b/cli/src/template_parser.rs index 3f7a2dfba..e14c5ecc3 100644 --- a/cli/src/template_parser.rs +++ b/cli/src/template_parser.rs @@ -26,6 +26,7 @@ use jj_lib::dsl_util::AliasExpandError; use jj_lib::dsl_util::AliasExpandableExpression; use jj_lib::dsl_util::AliasId; use jj_lib::dsl_util::AliasesMap; +use jj_lib::dsl_util::Diagnostics; use jj_lib::dsl_util::ExpressionFolder; use jj_lib::dsl_util::FoldableExpression; use jj_lib::dsl_util::InvalidArguments; @@ -84,6 +85,9 @@ impl Rule { } } +/// Manages diagnostic messages emitted during template parsing and building. +pub type TemplateDiagnostics = Diagnostics; + pub type TemplateParseResult = Result; #[derive(Debug, Error)]