diff --git a/Cargo.lock b/Cargo.lock index c639a9852e8..d8cbd26e9bf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -337,9 +337,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.12.0" +version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ae3f5d315924270530207e2a68396c3cc547f6dca3fbdca317cfb1a51edb593" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" dependencies = [ "serde", ] @@ -2635,6 +2635,46 @@ dependencies = [ "whoami", ] +[[package]] +name = "jj-core" +version = "0.43.0" +dependencies = [ + "assert_matches", + "blake2", + "chrono", + "clru", + "digest 0.10.7", + "etcetera", + "eyre", + "futures 0.3.33", + "insta", + "itertools 0.15.0", + "jj-core-proc-macros", + "pest", + "pest_derive", + "pollster", + "ref-cast", + "same-file", + "serde", + "smallvec", + "strsim", + "tempfile", + "test-case", + "thiserror 2.0.19", + "tokio", + "tracing", + "winreg", +] + +[[package]] +name = "jj-core-proc-macros" +version = "0.43.0" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + [[package]] name = "jj-lib" version = "0.43.0" @@ -2661,7 +2701,8 @@ dependencies = [ "insta", "interim", "itertools 0.15.0", - "jj-lib-proc-macros", + "jj-core", + "jj-core-proc-macros", "maplit", "memchr", "nix 0.31.3", @@ -2699,11 +2740,6 @@ dependencies = [ [[package]] name = "jj-lib-proc-macros" version = "0.43.0" -dependencies = [ - "proc-macro2", - "quote", - "syn 3.0.3", -] [[package]] name = "js-sys" @@ -3164,11 +3200,10 @@ dependencies = [ [[package]] name = "num-iter" -version = "0.1.45" +version = "0.1.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +checksum = "c92800bd69a1eac91786bcfe9da64a897eb72911b8dc3095decbd07429e8048b" dependencies = [ - "autocfg", "num-integer", "num-traits", ] diff --git a/Cargo.toml b/Cargo.toml index ebbdfbc26eb..61fff11c8e1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,7 @@ cargo-features = [] [workspace] resolver = "3" -members = ["cli", "lib", "lib/gen-protos", "lib/proc-macros", "lib/testutils"] +members = ["cli", "lib", "lib/core", "lib/gen-protos", "lib/proc-macros", "lib/testutils"] [workspace.package] version = "0.43.0" @@ -139,6 +139,8 @@ winreg = "0.56" # put all inter-workspace libraries, i.e. those that use 'path = ...' here in # their own (alphabetically sorted) block +jj-core = { path = "lib/core", version = "0.43.0", default-features = false } +jj-core-proc-macros = { path = "lib/core/proc-macros", version = "0.43.0" } jj-lib = { path = "lib", version = "0.43.0", default-features = false } jj-lib-proc-macros = { path = "lib/proc-macros", version = "0.43.0" } testutils = { path = "lib/testutils" } diff --git a/lib/Cargo.toml b/lib/Cargo.toml index 152fc1700c5..9f9da3e3849 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -47,7 +47,8 @@ hashbrown = { workspace = true } indexmap = { workspace = true } interim = { workspace = true } itertools = { workspace = true } -jj-lib-proc-macros = { workspace = true } +jj-core = { workspace = true } +jj-core-proc-macros = { workspace = true } maplit = { workspace = true } memchr = { workspace = true } once_cell = { workspace = true } diff --git a/lib/core/Cargo.toml b/lib/core/Cargo.toml new file mode 100644 index 00000000000..9dc18946568 --- /dev/null +++ b/lib/core/Cargo.toml @@ -0,0 +1,47 @@ +[package] +name = "jj-core" +version.workspace = true +license.workspace = true +rust-version.workspace = true +edition.workspace = true +readme.workspace = true +homepage.workspace = true +repository.workspace = true +documentation.workspace = true +categories.workspace = true +keywords.workspace = true + +[dependencies] + +blake2 = { workspace = true } +chrono = { workspace = true } +clru = { workspace = true } +digest = { workspace = true } +etcetera = { workspace = true } +futures = { workspace = true } +itertools = { workspace = true } +jj-core-proc-macros = { workspace = true } +pest = { workspace = true } +pest_derive = { workspace = true } +pollster = { workspace = true } +ref-cast = { workspace = true } +serde = { workspace = true } +smallvec = { workspace = true } +strsim = { workspace = true } +tempfile = { workspace = true } +thiserror = { workspace = true } +tokio = { workspace = true } +tracing = { workspace = true } + +[dev-dependencies] +assert_matches = { workspace = true } +eyre = { workspace = true } +insta = { workspace = true } +test-case = { workspace = true } + +[target.'cfg(windows)'.dependencies] +same-file = { workspace = true } +winreg = { workspace = true } + +[lints] +workspace = true diff --git a/lib/core/proc-macros/Cargo.toml b/lib/core/proc-macros/Cargo.toml new file mode 100644 index 00000000000..8c06760c634 --- /dev/null +++ b/lib/core/proc-macros/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "jj-core-proc-macros" +description = "Proc macros for the jj-core crate" + +categories.workspace = true +documentation.workspace = true +edition.workspace = true +homepage.workspace = true +keywords.workspace = true +license.workspace = true +readme.workspace = true +repository.workspace = true +rust-version.workspace = true +version.workspace = true +include = ["/LICENSE", "/src/"] + +[lib] +proc-macro = true + +[dependencies] +proc-macro2 = { workspace = true } +quote = { workspace = true } +syn = { workspace = true } + +[lints] +workspace = true diff --git a/lib/core/proc-macros/LICENSE b/lib/core/proc-macros/LICENSE new file mode 100644 index 00000000000..d6456956733 --- /dev/null +++ b/lib/core/proc-macros/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/lib/proc-macros/src/content_hash.rs b/lib/core/proc-macros/src/content_hash.rs similarity index 90% rename from lib/proc-macros/src/content_hash.rs rename to lib/core/proc-macros/src/content_hash.rs index e7d3d31b9ec..2123d143661 100644 --- a/lib/proc-macros/src/content_hash.rs +++ b/lib/core/proc-macros/src/content_hash.rs @@ -18,7 +18,7 @@ pub fn add_trait_bounds(mut generics: Generics) -> Generics { if let GenericParam::Type(type_param) = param { type_param .bounds - .push(parse_quote!(::jj_lib::content_hash::ContentHash)); + .push(parse_quote!(::jj_core::content_hash::ContentHash)); } } generics @@ -32,7 +32,7 @@ pub fn generate_hash_impl(data: &Data) -> TokenStream { let field_name = &f.ident; let ty = &f.ty; quote_spanned! {ty.span()=> - <#ty as ::jj_lib::content_hash::ContentHash>::hash( + <#ty as ::jj_core::content_hash::ContentHash>::hash( &self.#field_name, state); } }); @@ -45,7 +45,7 @@ pub fn generate_hash_impl(data: &Data) -> TokenStream { let index = Index::from(i); let ty = &f.ty; quote_spanned! {ty.span() => - <#ty as ::jj_lib::content_hash::ContentHash>::hash(&self.#index, state); + <#ty as ::jj_core::content_hash::ContentHash>::hash(&self.#index, state); } }); quote! { @@ -86,7 +86,7 @@ pub fn generate_hash_impl(data: &Data) -> TokenStream { let ix = index_to_ordinal(i); quote_spanned! {v.span() => Self::#variant_id => { - ::jj_lib::content_hash::ContentHash::hash(&#ix, state); + ::jj_core::content_hash::ContentHash::hash(&#ix, state); } } } @@ -137,10 +137,10 @@ fn hash_statements_for_enum_fields<'a>( let ix = index_to_ordinal(index); let typed_bindings = enum_bindings_with_type(fields); let mut hash_statements = Vec::with_capacity(typed_bindings.len() + 1); - hash_statements.push(quote! {::jj_lib::content_hash::ContentHash::hash(&#ix, state);}); + hash_statements.push(quote! {::jj_core::content_hash::ContentHash::hash(&#ix, state);}); for (ty, b) in &typed_bindings { hash_statements.push(quote_spanned! {b.span() => - <#ty as ::jj_lib::content_hash::ContentHash>::hash(#b, state); + <#ty as ::jj_core::content_hash::ContentHash>::hash(#b, state); }); } diff --git a/lib/core/proc-macros/src/lib.rs b/lib/core/proc-macros/src/lib.rs new file mode 100644 index 00000000000..1cb17071a93 --- /dev/null +++ b/lib/core/proc-macros/src/lib.rs @@ -0,0 +1,38 @@ +mod content_hash; + +extern crate proc_macro; + +use quote::quote; +use syn::DeriveInput; +use syn::parse_macro_input; + +/// Derive macro generating an impl of the trait `ContentHash`. +/// +/// Derives the `ContentHash` trait for a struct by calling `ContentHash::hash` +/// on each of the struct members in the order that they're declared. All +/// members of the struct must implement the `ContentHash` trait. +#[proc_macro_derive(ContentHash)] +pub fn derive_content_hash(input: proc_macro::TokenStream) -> proc_macro::TokenStream { + let input = parse_macro_input!(input as DeriveInput); + + // The name of the struct. + let name = &input.ident; + + // Generate an expression to hash each of the fields in the struct. + let hash_impl = content_hash::generate_hash_impl(&input.data); + + // Handle structs and enums with generics. + let generics = content_hash::add_trait_bounds(input.generics); + let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); + + let expanded = quote! { + #[automatically_derived] + impl #impl_generics ::jj_core::content_hash::ContentHash for #name #ty_generics + #where_clause { + fn hash(&self, state: &mut impl ::jj_core::content_hash::DigestUpdate) { + #hash_impl + } + } + }; + expanded.into() +} diff --git a/lib/core/src/backend.rs b/lib/core/src/backend.rs new file mode 100644 index 00000000000..ddd389cdc76 --- /dev/null +++ b/lib/core/src/backend.rs @@ -0,0 +1,45 @@ +// Copyright 2026 The Jujutsu Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Contains a basic shim for some Backend types such as [`ChangeId`] and +//! [`CommitId`]. +// TODO: move the `Backend` trait into this. + +use crate::hex_util; +use crate::object_id::ObjectId as _; +use crate::object_id::id_type; + +id_type!( + /// Identifier for a `Commit` based on its content. When a commit is + /// rewritten, its `CommitId` changes. + pub CommitId { hex() } +); +id_type!( + /// Stable identifier for a `Commit`. Unlike the `CommitId`, the `ChangeId` + /// follows the commit and is not updated when the commit is rewritten. + pub ChangeId { reverse_hex() } +); + +impl ChangeId { + /// Parses the given "reverse" hex string into a `ChangeId`. + pub fn try_from_reverse_hex(hex: impl AsRef<[u8]>) -> Option { + hex_util::decode_reverse_hex(hex).map(Self) + } + + /// Returns the hex string representation of this ID, which uses `z-k` + /// "digits" instead of `0-9a-f`. + pub fn reverse_hex(&self) -> String { + hex_util::encode_reverse_hex(&self.0) + } +} diff --git a/lib/core/src/content_hash.rs b/lib/core/src/content_hash.rs new file mode 100644 index 00000000000..932bab2eaf0 --- /dev/null +++ b/lib/core/src/content_hash.rs @@ -0,0 +1,313 @@ +// Copyright 2026 The Jujutsu Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Portable, stable hashing suitable for identifying values + +use blake2::Blake2b512; +// Re-export DigestUpdate so that the ContentHash proc macro can be used in +// external crates without directly depending on the digest crate. +pub use digest::Update as DigestUpdate; +use itertools::Itertools as _; +pub use jj_core_proc_macros::ContentHash; + +/// Portable, stable hashing suitable for identifying values +/// +/// Variable-length sequences should hash a 64-bit little-endian representation +/// of their length, then their elements in order. Unordered containers should +/// order their elements according to their `Ord` implementation. Enums should +/// hash a 32-bit little-endian encoding of the ordinal number of the enum +/// variant, then the variant's fields in lexical order. +/// +/// Structs can implement `ContentHash` by using `#[derive(ContentHash)]`. +pub trait ContentHash { + /// Update the hasher state with this object's content + fn hash(&self, state: &mut impl DigestUpdate); +} + +/// The 512-bit BLAKE2b content hash +pub fn blake2b_hash(x: &(impl ContentHash + ?Sized)) -> digest::Output { + use digest::Digest as _; + let mut hasher = Blake2b512::default(); + x.hash(&mut hasher); + hasher.finalize() +} + +impl ContentHash for () { + fn hash(&self, _: &mut impl DigestUpdate) {} +} + +macro_rules! tuple_impls { + ($( ( $($n:tt $T:ident),+ ) )+) => { + $( + impl<$($T: ContentHash,)+> ContentHash for ($($T,)+) { + fn hash(&self, state: &mut impl DigestUpdate) { + $(self.$n.hash(state);)+ + } + } + )+ + } +} + +tuple_impls! { + (0 T0) + (0 T0, 1 T1) + (0 T0, 1 T1, 2 T2) + (0 T0, 1 T1, 2 T2, 3 T3) +} + +impl ContentHash for bool { + fn hash(&self, state: &mut impl DigestUpdate) { + u8::from(*self).hash(state); + } +} + +impl ContentHash for u8 { + fn hash(&self, state: &mut impl DigestUpdate) { + state.update(&[*self]); + } +} + +impl ContentHash for u32 { + fn hash(&self, state: &mut impl DigestUpdate) { + state.update(&self.to_le_bytes()); + } +} + +impl ContentHash for i32 { + fn hash(&self, state: &mut impl DigestUpdate) { + state.update(&self.to_le_bytes()); + } +} + +impl ContentHash for u64 { + fn hash(&self, state: &mut impl DigestUpdate) { + state.update(&self.to_le_bytes()); + } +} + +impl ContentHash for i64 { + fn hash(&self, state: &mut impl DigestUpdate) { + state.update(&self.to_le_bytes()); + } +} + +// TODO: Specialize for [u8] once specialization exists +impl ContentHash for [T] { + fn hash(&self, state: &mut impl DigestUpdate) { + state.update(&(self.len() as u64).to_le_bytes()); + for x in self { + x.hash(state); + } + } +} + +impl ContentHash for Vec { + fn hash(&self, state: &mut impl DigestUpdate) { + self.as_slice().hash(state); + } +} + +impl ContentHash for str { + fn hash(&self, state: &mut impl DigestUpdate) { + self.as_bytes().hash(state); + } +} + +impl ContentHash for String { + fn hash(&self, state: &mut impl DigestUpdate) { + self.as_str().hash(state); + } +} + +impl ContentHash for Option { + fn hash(&self, state: &mut impl DigestUpdate) { + match self { + None => state.update(&0u32.to_le_bytes()), + Some(x) => { + state.update(&1u32.to_le_bytes()); + x.hash(state); + } + } + } +} + +impl ContentHash for std::collections::HashMap +where + K: ContentHash + Ord, + V: ContentHash, +{ + fn hash(&self, state: &mut impl DigestUpdate) { + state.update(&(self.len() as u64).to_le_bytes()); + let mut kv = self.iter().collect_vec(); + kv.sort_unstable_by_key(|&(k, _)| k); + for (k, v) in kv { + k.hash(state); + v.hash(state); + } + } +} + +impl ContentHash for std::collections::HashSet +where + K: ContentHash + Ord, +{ + fn hash(&self, state: &mut impl DigestUpdate) { + state.update(&(self.len() as u64).to_le_bytes()); + for k in self.iter().sorted() { + k.hash(state); + } + } +} + +impl ContentHash for std::collections::BTreeMap +where + K: ContentHash, + V: ContentHash, +{ + fn hash(&self, state: &mut impl DigestUpdate) { + state.update(&(self.len() as u64).to_le_bytes()); + for (k, v) in self { + k.hash(state); + v.hash(state); + } + } +} + +#[cfg(test)] +mod tests { + use std::collections::BTreeMap; + use std::collections::HashMap; + + use super::*; + use crate::hex_util; + + #[test] + fn test_string_sanity() { + let a = "a".to_string(); + let b = "b".to_string(); + assert_eq!(hash(&a), hash(&a.clone())); + assert_ne!(hash(&a), hash(&b)); + assert_ne!(hash(&"a".to_string()), hash(&"a\0".to_string())); + } + + #[test] + fn test_hash_map_key_value_distinction() { + let a = [("ab".to_string(), "cd".to_string())] + .into_iter() + .collect::>(); + let b = [("a".to_string(), "bcd".to_string())] + .into_iter() + .collect::>(); + + assert_ne!(hash(&a), hash(&b)); + } + + #[test] + fn test_btree_map_key_value_distinction() { + let a = [("ab".to_string(), "cd".to_string())] + .into_iter() + .collect::>(); + let b = [("a".to_string(), "bcd".to_string())] + .into_iter() + .collect::>(); + + assert_ne!(hash(&a), hash(&b)); + } + + #[test] + fn test_tuple_sanity() { + #[derive(ContentHash)] + struct T1(i32); + #[derive(ContentHash)] + struct T2(i32, i32); + #[derive(ContentHash)] + struct T3(i32, i32, i32); + #[derive(ContentHash)] + struct T4(i32, i32, i32, i32); + assert_eq!(hash(&T1(0)), hash(&(0,))); + assert_eq!(hash(&T2(0, 1)), hash(&(0, 1))); + assert_eq!(hash(&T3(0, 1, 2)), hash(&(0, 1, 2))); + assert_eq!(hash(&T4(0, 1, 2, 3)), hash(&(0, 1, 2, 3))); + } + + #[test] + fn test_struct_sanity() { + #[derive(ContentHash)] + struct Foo { + x: i32, + } + assert_ne!(hash(&Foo { x: 42 }), hash(&Foo { x: 12 })); + } + + #[test] + fn test_option_sanity() { + assert_ne!(hash(&Some(42)), hash(&42)); + assert_ne!(hash(&None::), hash(&42i32)); + } + + #[test] + fn test_slice_sanity() { + assert_ne!(hash(&[42i32][..]), hash(&[12i32][..])); + assert_ne!(hash(&([] as [i32; 0])[..]), hash(&[42i32][..])); + assert_ne!(hash(&([] as [i32; 0])[..]), hash(&())); + assert_ne!(hash(&42i32), hash(&[42i32][..])); + } + + #[test] + fn test_consistent_hashing() { + #[derive(ContentHash)] + struct Foo { + x: Vec>, + y: i64, + } + let foo_hash = hex_util::encode_hex(&hash(&Foo { + x: vec![None, Some(42)], + y: 17, + })); + insta::assert_snapshot!( + foo_hash, + @"e33c423b4b774b1353c414e0f9ef108822fde2fd5113fcd53bf7bd9e74e3206690b96af96373f268ed95dd020c7cbe171c7b7a6947fcaf5703ff6c8e208cefd4" + ); + + // Try again with an equivalent generic struct deriving ContentHash. + #[derive(ContentHash)] + struct GenericFoo { + x: X, + y: Y, + } + assert_eq!( + hex_util::encode_hex(&hash(&GenericFoo { + x: vec![None, Some(42)], + y: 17i64 + })), + foo_hash + ); + } + // Test that the derived version of `ContentHash` matches the that's + // manually implemented for `std::Option`. + #[test] + fn derive_for_enum() { + #[derive(ContentHash)] + enum MyOption { + None, + Some(T), + } + assert_eq!(hash(&Option::::None), hash(&MyOption::::None)); + assert_eq!(hash(&Some(1)), hash(&MyOption::Some(1))); + } + + fn hash(x: &(impl ContentHash + ?Sized)) -> digest::Output { + blake2b_hash(x) + } +} diff --git a/lib/core/src/dsl_util.rs b/lib/core/src/dsl_util.rs new file mode 100644 index 00000000000..0bad038f3f9 --- /dev/null +++ b/lib/core/src/dsl_util.rs @@ -0,0 +1,1087 @@ +// Copyright 2020-2024 The Jujutsu Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Domain-specific language helpers. + +use std::ascii; +use std::collections::HashMap; +use std::fmt; +use std::slice; + +use itertools::Itertools as _; +use pest::RuleType; +use pest::iterators::Pair; +use pest::iterators::Pairs; + +/// Manages diagnostic messages emitted during parsing. +/// +/// `T` is usually a parse error type of the language, which contains a message +/// and source span of 'static lifetime. +#[derive(Debug)] +pub struct Diagnostics { + // This might be extended to [{ kind: Warning|Error, message: T }, ..]. + diagnostics: Vec, +} + +impl Diagnostics { + /// Creates new empty diagnostics collector. + pub fn new() -> Self { + Self { + diagnostics: Vec::new(), + } + } + + /// Returns `true` if there are no diagnostic messages. + pub fn is_empty(&self) -> bool { + self.diagnostics.is_empty() + } + + /// Returns the number of diagnostic messages. + pub fn len(&self) -> usize { + self.diagnostics.len() + } + + /// Returns iterator over diagnostic messages. + pub fn iter(&self) -> slice::Iter<'_, T> { + self.diagnostics.iter() + } + + /// Adds a diagnostic message of warning level. + pub fn add_warning(&mut self, diag: T) { + self.diagnostics.push(diag); + } + + /// Moves diagnostic messages of different type (such as fileset warnings + /// emitted within `file()` revset.) + pub fn extend_with(&mut self, diagnostics: Diagnostics, mut f: impl FnMut(U) -> T) { + self.diagnostics + .extend(diagnostics.diagnostics.into_iter().map(&mut f)); + } +} + +impl Default for Diagnostics { + fn default() -> Self { + Self::new() + } +} + +impl<'a, T> IntoIterator for &'a Diagnostics { + type Item = &'a T; + type IntoIter = slice::Iter<'a, T>; + + fn into_iter(self) -> Self::IntoIter { + self.iter() + } +} + +/// AST node without type or name checking. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct ExpressionNode<'i, T> { + /// Expression item such as identifier, literal, function call, etc. + pub kind: T, + /// Span of the node. + pub span: pest::Span<'i>, +} + +impl<'i, T> ExpressionNode<'i, T> { + /// Wraps the given expression and span. + pub fn new(kind: T, span: pest::Span<'i>) -> Self { + Self { kind, span } + } +} + +/// `:` expression in AST. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct PatternNode<'i, T> { + /// Pattern name or type (such as `glob`.) + pub name: &'i str, + /// Span of the pattern name. + pub name_span: pest::Span<'i>, + /// Value expression. + pub value: ExpressionNode<'i, T>, +} + +/// Function call in AST. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct FunctionCallNode<'i, T> { + /// Function name. + pub name: &'i str, + /// Span of the function name. + pub name_span: pest::Span<'i>, + /// List of positional arguments. + pub args: Vec>, + /// List of keyword arguments. + pub keyword_args: Vec>, + /// Span of the arguments list. + pub args_span: pest::Span<'i>, +} + +/// Keyword argument pair in AST. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct KeywordArgument<'i, T> { + /// Parameter name. + pub name: &'i str, + /// Span of the parameter name. + pub name_span: pest::Span<'i>, + /// Value expression. + pub value: ExpressionNode<'i, T>, +} + +impl<'i, T> FunctionCallNode<'i, T> { + /// Number of arguments assuming named arguments are all unique. + pub fn arity(&self) -> usize { + self.args.len() + self.keyword_args.len() + } + + /// Ensures that no arguments passed. + pub fn expect_no_arguments(&self) -> Result<(), InvalidArguments<'i>> { + let ([], []) = self.expect_arguments()?; + Ok(()) + } + + /// Extracts exactly N required arguments. + pub fn expect_exact_arguments( + &self, + ) -> Result<&[ExpressionNode<'i, T>; N], InvalidArguments<'i>> { + let (args, []) = self.expect_arguments()?; + Ok(args) + } + + /// Extracts N required arguments and remainders. + /// + /// This can be used to get all the positional arguments without requiring + /// any (N = 0): + /// ```ignore + /// let ([], content_nodes) = function.expect_some_arguments()?; + /// ``` + /// Avoid accessing `function.args` directly, as that may allow keyword + /// arguments to be silently ignored. + #[expect(clippy::type_complexity)] + pub fn expect_some_arguments( + &self, + ) -> Result<(&[ExpressionNode<'i, T>; N], &[ExpressionNode<'i, T>]), InvalidArguments<'i>> { + self.ensure_no_keyword_arguments()?; + if self.args.len() >= N { + let (required, rest) = self.args.split_at(N); + Ok((required.try_into().unwrap(), rest)) + } else { + Err(self.invalid_arguments_count(N, None)) + } + } + + /// Extracts N required arguments and M optional arguments. + #[expect(clippy::type_complexity)] + pub fn expect_arguments( + &self, + ) -> Result< + ( + &[ExpressionNode<'i, T>; N], + [Option<&ExpressionNode<'i, T>>; M], + ), + InvalidArguments<'i>, + > { + self.ensure_no_keyword_arguments()?; + let count_range = N..=(N + M); + if count_range.contains(&self.args.len()) { + let (required, rest) = self.args.split_at(N); + let mut optional = rest.iter().map(Some).collect_vec(); + optional.resize(M, None); + Ok(( + required.try_into().unwrap(), + optional.try_into().ok().unwrap(), + )) + } else { + let (min, max) = count_range.into_inner(); + Err(self.invalid_arguments_count(min, Some(max))) + } + } + + /// Extracts N required arguments and M optional arguments. Some of them can + /// be specified as keyword arguments. + /// + /// `names` is a list of parameter names. Unnamed positional arguments + /// should be padded with `""`. + #[expect(clippy::type_complexity)] + pub fn expect_named_arguments( + &self, + names: &[&str], + ) -> Result< + ( + [&ExpressionNode<'i, T>; N], + [Option<&ExpressionNode<'i, T>>; M], + ), + InvalidArguments<'i>, + > { + if self.keyword_args.is_empty() { + let (required, optional) = self.expect_arguments::()?; + Ok((required.each_ref(), optional)) + } else { + let (required, optional) = self.expect_named_arguments_vec(names, N, N + M)?; + Ok(( + required.try_into().ok().unwrap(), + optional.try_into().ok().unwrap(), + )) + } + } + + #[expect(clippy::type_complexity)] + fn expect_named_arguments_vec( + &self, + names: &[&str], + min: usize, + max: usize, + ) -> Result< + ( + Vec<&ExpressionNode<'i, T>>, + Vec>>, + ), + InvalidArguments<'i>, + > { + assert!(names.len() <= max); + + if self.args.len() > max { + return Err(self.invalid_arguments_count(min, Some(max))); + } + let mut extracted = Vec::with_capacity(max); + extracted.extend(self.args.iter().map(Some)); + extracted.resize(max, None); + + for arg in &self.keyword_args { + let name = arg.name; + let span = arg.name_span.start_pos().span(&arg.value.span.end_pos()); + let pos = names.iter().position(|&n| n == name).ok_or_else(|| { + self.invalid_arguments(format!(r#"Unexpected keyword argument "{name}""#), span) + })?; + if extracted[pos].is_some() { + return Err(self.invalid_arguments( + format!(r#"Got multiple values for keyword "{name}""#), + span, + )); + } + extracted[pos] = Some(&arg.value); + } + + let optional = extracted.split_off(min); + let required = extracted.into_iter().flatten().collect_vec(); + if required.len() != min { + return Err(self.invalid_arguments_count(min, Some(max))); + } + Ok((required, optional)) + } + + fn ensure_no_keyword_arguments(&self) -> Result<(), InvalidArguments<'i>> { + if let (Some(first), Some(last)) = (self.keyword_args.first(), self.keyword_args.last()) { + let span = first.name_span.start_pos().span(&last.value.span.end_pos()); + Err(self.invalid_arguments("Unexpected keyword arguments".to_owned(), span)) + } else { + Ok(()) + } + } + + fn invalid_arguments(&self, message: String, span: pest::Span<'i>) -> InvalidArguments<'i> { + InvalidArguments { + name: self.name, + message, + span, + } + } + + fn invalid_arguments_count(&self, min: usize, max: Option) -> InvalidArguments<'i> { + let message = match (min, max) { + (min, Some(max)) if min == max => format!("Expected {min} arguments"), + (min, Some(max)) => format!("Expected {min} to {max} arguments"), + (min, None) => format!("Expected at least {min} arguments"), + }; + self.invalid_arguments(message, self.args_span) + } + + fn invalid_arguments_count_with_arities( + &self, + arities: impl IntoIterator, + ) -> InvalidArguments<'i> { + let message = format!("Expected {} arguments", arities.into_iter().join(", ")); + self.invalid_arguments(message, self.args_span) + } +} + +/// Unexpected number of arguments, or invalid combination of arguments. +/// +/// This error is supposed to be converted to language-specific parse error +/// type, where lifetime `'i` will be eliminated. +#[derive(Clone, Debug)] +pub struct InvalidArguments<'i> { + /// Function name. + pub name: &'i str, + /// Error message. + pub message: String, + /// Span of the bad arguments. + pub span: pest::Span<'i>, +} + +/// Expression item that can be transformed recursively by using `folder: F`. +pub trait FoldableExpression<'i>: Sized { + /// Transforms `self` by applying the `folder` to inner items. + fn fold(self, folder: &mut F, span: pest::Span<'i>) -> Result + where + F: ExpressionFolder<'i, Self> + ?Sized; +} + +/// Visitor-like interface to transform AST nodes recursively. +pub trait ExpressionFolder<'i, T: FoldableExpression<'i>> { + /// Transform error. + type Error; + + /// Transforms the expression `node`. By default, inner items are + /// transformed recursively. + fn fold_expression( + &mut self, + node: ExpressionNode<'i, T>, + ) -> Result, Self::Error> { + let ExpressionNode { kind, span } = node; + let kind = kind.fold(self, span)?; + Ok(ExpressionNode { kind, span }) + } + + /// Transforms identifier. + fn fold_identifier(&mut self, name: &'i str, span: pest::Span<'i>) -> Result; + + /// Transforms pattern. + fn fold_pattern( + &mut self, + pattern: Box>, + span: pest::Span<'i>, + ) -> Result; + + /// Transforms function call. + fn fold_function_call( + &mut self, + function: Box>, + span: pest::Span<'i>, + ) -> Result; +} + +/// Transforms list of `nodes` by using `folder`. +pub fn fold_expression_nodes<'i, F, T>( + folder: &mut F, + nodes: Vec>, +) -> Result>, F::Error> +where + F: ExpressionFolder<'i, T> + ?Sized, + T: FoldableExpression<'i>, +{ + nodes + .into_iter() + .map(|node| folder.fold_expression(node)) + .try_collect() +} + +/// Transforms pattern value by using `folder`. +pub fn fold_pattern_value<'i, F, T>( + folder: &mut F, + pattern: PatternNode<'i, T>, +) -> Result, F::Error> +where + F: ExpressionFolder<'i, T> + ?Sized, + T: FoldableExpression<'i>, +{ + Ok(PatternNode { + name: pattern.name, + name_span: pattern.name_span, + value: folder.fold_expression(pattern.value)?, + }) +} + +/// Transforms function call arguments by using `folder`. +pub fn fold_function_call_args<'i, F, T>( + folder: &mut F, + function: FunctionCallNode<'i, T>, +) -> Result, F::Error> +where + F: ExpressionFolder<'i, T> + ?Sized, + T: FoldableExpression<'i>, +{ + Ok(FunctionCallNode { + name: function.name, + name_span: function.name_span, + args: fold_expression_nodes(folder, function.args)?, + keyword_args: function + .keyword_args + .into_iter() + .map(|arg| { + Ok(KeywordArgument { + name: arg.name, + name_span: arg.name_span, + value: folder.fold_expression(arg.value)?, + }) + }) + .try_collect()?, + args_span: function.args_span, + }) +} + +/// Helper to parse string literal. +#[derive(Debug)] +pub struct StringLiteralParser { + /// String content part. + pub content_rule: R, + /// Escape sequence part including backslash character. + pub escape_rule: R, +} + +impl StringLiteralParser { + /// Parses the given string literal `pairs` into string. + pub fn parse(&self, pairs: Pairs) -> String { + let mut result = String::new(); + for part in pairs { + if part.as_rule() == self.content_rule { + result.push_str(part.as_str()); + } else if part.as_rule() == self.escape_rule { + match &part.as_str()[1..] { + "\"" => result.push('"'), + "\\" => result.push('\\'), + "t" => result.push('\t'), + "r" => result.push('\r'), + "n" => result.push('\n'), + "0" => result.push('\0'), + "e" => result.push('\x1b'), + hex if hex.starts_with('x') => { + result.push(char::from( + u8::from_str_radix(&hex[1..], 16).expect("hex characters"), + )); + } + char => panic!("invalid escape: \\{char:?}"), + } + } else { + panic!("unexpected part of string: {part:?}"); + } + } + result + } +} + +/// Escape special characters in the input +pub fn escape_string(unescaped: &str) -> String { + let mut escaped = String::with_capacity(unescaped.len()); + for c in unescaped.chars() { + match c { + '"' => escaped.push_str(r#"\""#), + '\\' => escaped.push_str(r#"\\"#), + '\t' => escaped.push_str(r#"\t"#), + '\r' => escaped.push_str(r#"\r"#), + '\n' => escaped.push_str(r#"\n"#), + '\0' => escaped.push_str(r#"\0"#), + c if c.is_ascii_control() => { + for b in ascii::escape_default(c as u8) { + escaped.push(b as char); + } + } + c => escaped.push(c), + } + } + escaped +} + +/// Helper to parse function call. +#[derive(Debug)] +pub struct FunctionCallParser { + /// Function name. + pub function_name_rule: R, + /// List of positional and keyword arguments. + pub function_arguments_rule: R, + /// Pair of parameter name and value. + pub keyword_argument_rule: R, + /// Parameter name. + pub argument_name_rule: R, + /// Value expression. + pub argument_value_rule: R, +} + +impl FunctionCallParser { + /// Parses the given `pair` as function call. + pub fn parse<'i, T, E: From>>( + &self, + pair: Pair<'i, R>, + // parse_name can be defined for any Pair<'_, R>, but parse_value should + // be allowed to construct T by capturing Pair<'i, R>. + parse_name: impl Fn(Pair<'i, R>) -> Result<&'i str, E>, + parse_value: impl Fn(Pair<'i, R>) -> Result, E>, + ) -> Result, E> { + let [name_pair, args_pair] = pair.into_inner().collect_array().unwrap(); + assert_eq!(name_pair.as_rule(), self.function_name_rule); + assert_eq!(args_pair.as_rule(), self.function_arguments_rule); + let name_span = name_pair.as_span(); + let args_span = args_pair.as_span(); + let function_name = parse_name(name_pair)?; + let mut args = Vec::new(); + let mut keyword_args = Vec::new(); + for pair in args_pair.into_inner() { + let span = pair.as_span(); + if pair.as_rule() == self.argument_value_rule { + if !keyword_args.is_empty() { + return Err(InvalidArguments { + name: function_name, + message: "Positional argument follows keyword argument".to_owned(), + span, + } + .into()); + } + args.push(parse_value(pair)?); + } else if pair.as_rule() == self.keyword_argument_rule { + let [name_pair, value_pair] = pair.into_inner().collect_array().unwrap(); + assert_eq!(name_pair.as_rule(), self.argument_name_rule); + assert_eq!(value_pair.as_rule(), self.argument_value_rule); + let name_span = name_pair.as_span(); + let arg = KeywordArgument { + name: parse_name(name_pair)?, + name_span, + value: parse_value(value_pair)?, + }; + keyword_args.push(arg); + } else { + panic!("unexpected argument rule {pair:?}"); + } + } + Ok(FunctionCallNode { + name: function_name, + name_span, + args, + keyword_args, + args_span, + }) + } +} + +/// A function alias containing `(params, definition, description)`. +type FunctionAlias = (Vec, V, Option); + +/// Map of symbol, pattern, and function aliases. +#[derive(Clone, Debug, Default)] +pub struct AliasesMap { + symbol_aliases: HashMap)>, + // name: (param, defn) + pattern_aliases: HashMap)>, + // name: [(params, defn)] (sorted by arity) + function_aliases: HashMap>>, + // Parser type P helps prevent misuse of AliasesMap of different language. + parser: P, +} + +impl AliasesMap { + /// Creates an empty aliases map with default-constructed parser. + pub fn new() -> Self + where + P: Default, + { + Self { + symbol_aliases: Default::default(), + pattern_aliases: Default::default(), + function_aliases: Default::default(), + parser: Default::default(), + } + } + + /// Adds new substitution rule `decl = defn`. + /// + /// Returns error if `decl` is invalid. The `defn` part isn't checked. A bad + /// `defn` will be reported when the alias is substituted. + pub fn insert( + &mut self, + decl: impl AsRef, + defn: impl Into, + doc: Option, + ) -> Result<(), P::Error> + where + P: AliasDeclarationParser, + { + match self.parser.parse_declaration(decl.as_ref())? { + AliasDeclaration::Symbol(name) => { + self.symbol_aliases.insert(name, (defn.into(), doc)); + } + AliasDeclaration::Pattern(name, param) => { + self.pattern_aliases.insert(name, (param, defn.into(), doc)); + } + AliasDeclaration::Function(name, params) => { + let overloads = self.function_aliases.entry(name).or_default(); + match overloads.binary_search_by_key(¶ms.len(), |(params, _, _)| params.len()) { + Ok(i) => overloads[i] = (params, defn.into(), doc), + Err(i) => overloads.insert(i, (params, defn.into(), doc)), + } + } + } + Ok(()) + } + + /// Iterates symbol names in arbitrary order. + pub fn symbol_names(&self) -> impl Iterator { + self.symbol_aliases.keys().map(|n| n.as_ref()) + } + + /// Iterates pattern names in arbitrary order. + pub fn pattern_names(&self) -> impl Iterator { + self.pattern_aliases.keys().map(|n| n.as_ref()) + } + + /// Iterates function names in arbitrary order. + pub fn function_names(&self) -> impl Iterator { + self.function_aliases.keys().map(|n| n.as_ref()) + } + + /// Looks up symbol alias by name. Returns identifier, definition text, and + /// optional description. + pub fn get_symbol(&self, name: &str) -> Option<(AliasId<'_>, &V, Option<&str>)> { + self.symbol_aliases + .get_key_value(name) + .map(|(name, (defn, doc))| (AliasId::Symbol(name), defn, doc.as_deref())) + } + + /// Looks up pattern alias by name. Returns identifier, parameter name, + /// definition text, and optional description. + pub fn get_pattern(&self, name: &str) -> Option<(AliasId<'_>, &str, &V, Option<&str>)> { + self.pattern_aliases + .get_key_value(name) + .map(|(name, (param, defn, doc))| { + ( + AliasId::Pattern(name, param), + param.as_ref(), + defn, + doc.as_deref(), + ) + }) + } + + /// Looks up function alias by name and arity. Returns identifier, list of + /// parameter names, definition text, and optional description. + pub fn get_function( + &self, + name: &str, + arity: usize, + ) -> Option<(AliasId<'_>, &[String], &V, Option<&str>)> { + let overloads = self.get_function_overloads(name)?; + overloads.find_by_arity(arity) + } + + /// Looks up function aliases by name. + fn get_function_overloads(&self, name: &str) -> Option> { + let (name, overloads) = self.function_aliases.get_key_value(name)?; + Some(AliasFunctionOverloads { name, overloads }) + } +} + +#[derive(Clone, Debug)] +struct AliasFunctionOverloads<'a, V> { + name: &'a String, + overloads: &'a Vec<(Vec, V, Option)>, +} + +impl<'a, V> AliasFunctionOverloads<'a, V> { + fn arities(&self) -> impl DoubleEndedIterator + ExactSizeIterator { + self.overloads.iter().map(|(params, _, _)| params.len()) + } + + fn min_arity(&self) -> usize { + self.arities().next().unwrap() + } + + fn max_arity(&self) -> usize { + self.arities().next_back().unwrap() + } + + fn find_by_arity( + &self, + arity: usize, + ) -> Option<(AliasId<'a>, &'a [String], &'a V, Option<&'a str>)> { + let index = self + .overloads + .binary_search_by_key(&arity, |(params, _, _)| params.len()) + .ok()?; + let (params, defn, doc) = &self.overloads[index]; + // Exact parameter names aren't needed to identify a function, but they + // provide a better error indication. (e.g. "foo(x, y)" is easier to + // follow than "foo/2".) + Some(( + AliasId::Function(self.name, params), + params, + defn, + doc.as_deref(), + )) + } +} + +/// Borrowed reference to identify alias expression. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum AliasId<'a> { + /// Symbol name. + Symbol(&'a str), + /// Pattern name and parameter name. + Pattern(&'a str, &'a str), + /// Function name and parameter names. + Function(&'a str, &'a [String]), + /// Function parameter name. + Parameter(&'a str), +} + +impl fmt::Display for AliasId<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Symbol(name) => write!(f, "{name}"), + Self::Pattern(name, param) => write!(f, "{name}:{param}"), + Self::Function(name, params) => { + write!(f, "{name}({params})", params = params.join(", ")) + } + Self::Parameter(name) => write!(f, "{name}"), + } + } +} + +/// Parsed declaration part of alias rule. +#[derive(Clone, Debug)] +pub enum AliasDeclaration { + /// Symbol name. + Symbol(String), + /// Pattern name and parameter. + Pattern(String, String), + /// Function name and parameters. + Function(String, Vec), +} + +// AliasDeclarationParser and AliasDefinitionParser can be merged into a single +// trait, but it's unclear whether doing that would simplify the abstraction. + +/// Parser for symbol and function alias declaration. +pub trait AliasDeclarationParser { + /// Parse error type. + type Error; + + /// Parses symbol or function name and parameters. + fn parse_declaration(&self, source: &str) -> Result; +} + +/// Parser for symbol and function alias definition. +pub trait AliasDefinitionParser { + /// Expression item type. + type Output<'i>; + /// Parse error type. + type Error; + + /// Parses alias body. + fn parse_definition<'i>( + &self, + source: &'i str, + ) -> Result>, Self::Error>; +} + +/// Expression item that supports alias substitution. +pub trait AliasExpandableExpression<'i>: FoldableExpression<'i> { + /// Wraps identifier. + fn identifier(name: &'i str) -> Self; + /// Wraps pattern. + fn pattern(pattern: Box>) -> Self; + /// Wraps function call. + fn function_call(function: Box>) -> Self; + /// Wraps substituted expression. + fn alias_expanded(id: AliasId<'i>, subst: Box>) -> Self; +} + +/// Error that may occur during alias substitution. +pub trait AliasExpandError: Sized { + /// Unexpected number of arguments, or invalid combination of arguments. + fn invalid_arguments(err: InvalidArguments<'_>) -> Self; + /// Recursion detected during alias substitution. + fn recursive_expansion(id: AliasId<'_>, span: pest::Span<'_>) -> Self; + /// Attaches alias trace to the current error. + fn within_alias_expansion(self, id: AliasId<'_>, span: pest::Span<'_>) -> Self; +} + +/// Expands aliases recursively in tree of `T`. +#[derive(Debug)] +struct AliasExpander<'i, 'a, T, P> { + /// Alias symbols and functions that are globally available. + aliases_map: &'i AliasesMap, + /// Local variables set in the outermost scope. + locals: &'a HashMap<&'i str, ExpressionNode<'i, T>>, + /// Stack of aliases and local parameters currently expanding. + states: Vec>, +} + +#[derive(Debug)] +struct AliasExpandingState<'i, T> { + id: AliasId<'i>, + locals: HashMap<&'i str, ExpressionNode<'i, T>>, +} + +impl<'i, T, P, E> AliasExpander<'i, '_, T, P> +where + T: AliasExpandableExpression<'i> + Clone, + P: AliasDefinitionParser = T, Error = E>, + E: AliasExpandError, +{ + /// Local variables available to the current scope. + fn current_locals(&self) -> &HashMap<&'i str, ExpressionNode<'i, T>> { + self.states.last().map_or(self.locals, |s| &s.locals) + } + + fn expand_defn( + &mut self, + id: AliasId<'i>, + defn: &'i str, + locals: HashMap<&'i str, ExpressionNode<'i, T>>, + span: pest::Span<'i>, + ) -> Result { + // The stack should be short, so let's simply do linear search. + if self.states.iter().any(|s| s.id == id) { + return Err(E::recursive_expansion(id, span)); + } + self.states.push(AliasExpandingState { id, locals }); + // Parsed defn could be cached if needed. + let result = self + .aliases_map + .parser + .parse_definition(defn) + .and_then(|node| self.fold_expression(node)) + .map(|node| T::alias_expanded(id, Box::new(node))) + .map_err(|e| e.within_alias_expansion(id, span)); + self.states.pop(); + result + } +} + +impl<'i, T, P, E> ExpressionFolder<'i, T> for AliasExpander<'i, '_, T, P> +where + T: AliasExpandableExpression<'i> + Clone, + P: AliasDefinitionParser = T, Error = E>, + E: AliasExpandError, +{ + type Error = E; + + fn fold_identifier(&mut self, name: &'i str, span: pest::Span<'i>) -> Result { + if let Some(subst) = self.current_locals().get(name) { + let id = AliasId::Parameter(name); + Ok(T::alias_expanded(id, Box::new(subst.clone()))) + } else if let Some((id, defn, _doc)) = self.aliases_map.get_symbol(name) { + let locals = HashMap::new(); // Don't spill out the current scope + self.expand_defn(id, defn, locals, span) + } else { + Ok(T::identifier(name)) + } + } + + fn fold_pattern( + &mut self, + pattern: Box>, + span: pest::Span<'i>, + ) -> Result { + if let Some((id, param, defn, _doc)) = self.aliases_map.get_pattern(pattern.name) { + // Resolve argument in the current scope, and pass it in to the + // alias expansion scope. + let arg = self.fold_expression(pattern.value)?; + let locals = HashMap::from([(param, arg)]); + self.expand_defn(id, defn, locals, span) + } else { + let pattern = Box::new(fold_pattern_value(self, *pattern)?); + Ok(T::pattern(pattern)) + } + } + + fn fold_function_call( + &mut self, + function: Box>, + span: pest::Span<'i>, + ) -> Result { + // For better error indication, builtin functions are shadowed by name, + // not by (name, arity). + if let Some(overloads) = self.aliases_map.get_function_overloads(function.name) { + // TODO: add support for keyword arguments + function + .ensure_no_keyword_arguments() + .map_err(E::invalid_arguments)?; + let Some((id, params, defn, _doc)) = overloads.find_by_arity(function.arity()) else { + let min = overloads.min_arity(); + let max = overloads.max_arity(); + let err = if max - min + 1 == overloads.arities().len() { + function.invalid_arguments_count(min, Some(max)) + } else { + function.invalid_arguments_count_with_arities(overloads.arities()) + }; + return Err(E::invalid_arguments(err)); + }; + // Resolve arguments in the current scope, and pass them in to the alias + // expansion scope. + let args = fold_expression_nodes(self, function.args)?; + let locals = params.iter().map(|s| s.as_str()).zip(args).collect(); + self.expand_defn(id, defn, locals, span) + } else { + let function = Box::new(fold_function_call_args(self, *function)?); + Ok(T::function_call(function)) + } + } +} + +/// Expands aliases recursively. +pub fn expand_aliases<'i, T, P>( + node: ExpressionNode<'i, T>, + aliases_map: &'i AliasesMap, +) -> Result, P::Error> +where + T: AliasExpandableExpression<'i> + Clone, + P: AliasDefinitionParser = T>, + P::Error: AliasExpandError, +{ + expand_aliases_with_locals(node, aliases_map, &HashMap::new()) +} + +/// Expands aliases recursively with the outermost local variables. +/// +/// Local variables are similar to alias symbols, but are scoped. Alias symbols +/// are globally accessible from alias expressions, but local variables aren't. +pub fn expand_aliases_with_locals<'i, T, P>( + node: ExpressionNode<'i, T>, + aliases_map: &'i AliasesMap, + locals: &HashMap<&'i str, ExpressionNode<'i, T>>, +) -> Result, P::Error> +where + T: AliasExpandableExpression<'i> + Clone, + P: AliasDefinitionParser = T>, + P::Error: AliasExpandError, +{ + let mut expander = AliasExpander { + aliases_map, + locals, + states: Vec::new(), + }; + expander.fold_expression(node) +} + +/// Collects similar names from the `candidates` list. +pub fn collect_similar(name: &str, candidates: I) -> Vec +where + I: IntoIterator, + I::Item: AsRef, +{ + candidates + .into_iter() + .filter(|cand| { + // The parameter is borrowed from clap f5540d26 + strsim::jaro(name, cand.as_ref()) > 0.7 + }) + .map(|s| s.as_ref().to_owned()) + .sorted_unstable() + .collect() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_expect_arguments() { + fn empty_span() -> pest::Span<'static> { + pest::Span::new("", 0, 0).unwrap() + } + + fn function( + name: &'static str, + args: impl Into>>, + keyword_args: impl Into>>, + ) -> FunctionCallNode<'static, u32> { + FunctionCallNode { + name, + name_span: empty_span(), + args: args.into(), + keyword_args: keyword_args.into(), + args_span: empty_span(), + } + } + + fn value(v: u32) -> ExpressionNode<'static, u32> { + ExpressionNode::new(v, empty_span()) + } + + fn keyword(name: &'static str, v: u32) -> KeywordArgument<'static, u32> { + KeywordArgument { + name, + name_span: empty_span(), + value: value(v), + } + } + + let f = function("foo", [], []); + assert!(f.expect_no_arguments().is_ok()); + assert!(f.expect_some_arguments::<0>().is_ok()); + assert!(f.expect_arguments::<0, 0>().is_ok()); + assert!(f.expect_named_arguments::<0, 0>(&[]).is_ok()); + + let f = function("foo", [value(0)], []); + assert!(f.expect_no_arguments().is_err()); + assert_eq!( + f.expect_some_arguments::<0>().unwrap(), + (&[], [value(0)].as_slice()) + ); + assert_eq!( + f.expect_some_arguments::<1>().unwrap(), + (&[value(0)], [].as_slice()) + ); + assert!(f.expect_arguments::<0, 0>().is_err()); + assert_eq!( + f.expect_arguments::<0, 1>().unwrap(), + (&[], [Some(&value(0))]) + ); + assert_eq!(f.expect_arguments::<1, 1>().unwrap(), (&[value(0)], [None])); + assert!(f.expect_named_arguments::<0, 0>(&[]).is_err()); + assert_eq!( + f.expect_named_arguments::<0, 1>(&["a"]).unwrap(), + ([], [Some(&value(0))]) + ); + assert_eq!( + f.expect_named_arguments::<1, 0>(&["a"]).unwrap(), + ([&value(0)], []) + ); + + let f = function("foo", [], [keyword("a", 0)]); + assert!(f.expect_no_arguments().is_err()); + assert!(f.expect_some_arguments::<1>().is_err()); + assert!(f.expect_arguments::<0, 1>().is_err()); + assert!(f.expect_arguments::<1, 0>().is_err()); + assert!(f.expect_named_arguments::<0, 0>(&[]).is_err()); + assert!(f.expect_named_arguments::<0, 1>(&[]).is_err()); + assert!(f.expect_named_arguments::<1, 0>(&[]).is_err()); + assert_eq!( + f.expect_named_arguments::<1, 0>(&["a"]).unwrap(), + ([&value(0)], []) + ); + assert_eq!( + f.expect_named_arguments::<1, 1>(&["a", "b"]).unwrap(), + ([&value(0)], [None]) + ); + assert!(f.expect_named_arguments::<1, 1>(&["b", "a"]).is_err()); + + let f = function("foo", [value(0)], [keyword("a", 1), keyword("b", 2)]); + assert!(f.expect_named_arguments::<0, 0>(&[]).is_err()); + assert!(f.expect_named_arguments::<1, 1>(&["a", "b"]).is_err()); + assert_eq!( + f.expect_named_arguments::<1, 2>(&["c", "a", "b"]).unwrap(), + ([&value(0)], [Some(&value(1)), Some(&value(2))]) + ); + assert_eq!( + f.expect_named_arguments::<2, 1>(&["c", "b", "a"]).unwrap(), + ([&value(0), &value(2)], [Some(&value(1))]) + ); + assert_eq!( + f.expect_named_arguments::<0, 3>(&["c", "b", "a"]).unwrap(), + ([], [Some(&value(0)), Some(&value(2)), Some(&value(1))]) + ); + + let f = function("foo", [], [keyword("a", 0), keyword("a", 1)]); + assert!(f.expect_named_arguments::<1, 1>(&["", "a"]).is_err()); + } +} diff --git a/lib/core/src/file_util.rs b/lib/core/src/file_util.rs new file mode 100644 index 00000000000..1b877b54c30 --- /dev/null +++ b/lib/core/src/file_util.rs @@ -0,0 +1,694 @@ +// Copyright 2021 The Jujutsu Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Contains file utilities which work in a cross-platform way. + +use std::borrow::Cow; +use std::ffi::OsString; +use std::fs; +use std::fs::File; +use std::io; +use std::io::ErrorKind; +use std::io::Write; +use std::path::Component; +use std::path::Path; +use std::path::PathBuf; + +use futures::AsyncRead; +use futures::AsyncReadExt as _; +use tempfile::NamedTempFile; +use tempfile::PersistError; +use thiserror::Error; + +#[cfg(unix)] +pub use self::platform::check_executable_bit_support; +pub use self::platform::check_symlink_support; +pub use self::platform::symlink_dir; +pub use self::platform::symlink_file; + +/// An error which can occur when accessing paths. +#[derive(Debug, Error)] +#[error("Cannot access {path}")] +pub struct PathError { + /// The path which is inaccessible. + pub path: PathBuf, + /// The underlying error source. + pub source: io::Error, +} + +/// An extension trait to `io::Result` to allow adding a path as context. +pub trait IoResultExt { + /// Provide more context to the `io::Result`. + fn context(self, path: impl AsRef) -> Result; +} + +impl IoResultExt for io::Result { + fn context(self, path: impl AsRef) -> Result { + self.map_err(|error| PathError { + path: path.as_ref().to_path_buf(), + source: error, + }) + } +} + +/// Creates a directory or does nothing if the directory already exists. +/// +/// Returns the underlying error if the directory can't be created. +/// The function will also fail if intermediate directories on the path do not +/// already exist. +pub fn create_or_reuse_dir(dirname: &Path) -> io::Result<()> { + match fs::create_dir(dirname) { + Ok(()) => Ok(()), + Err(_) if dirname.is_dir() => Ok(()), + Err(e) => Err(e), + } +} + +/// Removes all files in the directory, but not the directory itself. +/// +/// The directory must exist, and there should be no sub directories. +pub fn remove_dir_contents(dirname: &Path) -> Result<(), PathError> { + for entry in dirname.read_dir().context(dirname)? { + let entry = entry.context(dirname)?; + let path = entry.path(); + fs::remove_file(&path).context(&path)?; + } + Ok(()) +} + +/// Checks if path points at an empty directory. +pub fn is_empty_dir(path: &Path) -> Result { + match path.read_dir() { + Ok(mut entries) => Ok(entries.next().is_none()), + Err(error) => match error.kind() { + ErrorKind::NotADirectory => Ok(false), + ErrorKind::NotFound => Ok(false), + _ => Err(error).context(path)?, + }, + } +} + +/// An error which occurs when we encounter a path which isn't UTF-8 encoded. +#[derive(Debug, Error)] +#[error(transparent)] +pub struct BadPathEncoding(platform::BadOsStrEncoding); + +/// Constructs [`Path`] from `bytes` in platform-specific manner. +/// +/// On Unix, this function never fails because paths are just bytes. On Windows, +/// this may return error if the input wasn't well-formed UTF-8. +pub fn path_from_bytes(bytes: &[u8]) -> Result<&Path, BadPathEncoding> { + let s = platform::os_str_from_bytes(bytes).map_err(BadPathEncoding)?; + Ok(Path::new(s)) +} + +/// Converts `path` to bytes in platform-specific manner. +/// +/// On Unix, this function never fails because paths are just bytes. On Windows, +/// this may return error if the input wasn't well-formed UTF-8. +/// +/// The returned byte sequence can be considered a superset of ASCII (such as +/// UTF-8 bytes.) +pub fn path_to_bytes(path: &Path) -> Result<&[u8], BadPathEncoding> { + platform::os_str_to_bytes(path.as_ref()).map_err(BadPathEncoding) +} + +/// Expands "~/" to the user's home directory. +pub fn expand_home_path(path_str: &str) -> PathBuf { + if let Some(remainder) = path_str.strip_prefix("~/") + && let Ok(home_dir) = etcetera::home_dir() + { + return home_dir.join(remainder); + } + PathBuf::from(path_str) +} + +/// Turns the given `to` path into relative path starting from the `from` path. +/// +/// Both `from` and `to` paths are supposed to be absolute and normalized in the +/// same manner. If `from` and `to` share no common prefix, the returned path is +/// unchanged. This also means `relative_path(abs, rel)` will return `rel`. +pub fn relative_path(from: &Path, to: &Path) -> PathBuf { + let Some((from_suffix, to_suffix)) = strip_common_path_prefix(from, to) else { + // No common prefix found. Return the original path. + return to.to_owned(); + }; + let depth = from_suffix.components().count(); + let mut relative = PathBuf::with_capacity(2 * depth + 1 + to_suffix.as_os_str().len()); + for _ in 0..depth { + relative.push(Component::ParentDir); + } + if !to_suffix.as_os_str().is_empty() { + relative.push(to_suffix); + } else if depth == 0 { + relative.push(Component::CurDir); + } + relative +} + +fn strip_common_path_prefix<'a, 'b>( + path1: &'a Path, + path2: &'b Path, +) -> Option<(&'a Path, &'b Path)> { + let mut components1 = path1.components(); + let mut components2 = path2.components(); + let mut suffix_paths = None; + while let (Some(c1), Some(c2)) = (components1.next(), components2.next()) { + if c1 != c2 { + break; + } + suffix_paths = Some((components1.as_path(), components2.as_path())); + } + suffix_paths +} + +/// Consumes as much `..` and `.` as possible without considering symlinks. +pub fn normalize_path(path: &Path) -> PathBuf { + let mut result = PathBuf::new(); + for c in path.components() { + match c { + Component::CurDir => {} + Component::ParentDir + if matches!(result.components().next_back(), Some(Component::Normal(_))) => + { + // Do not pop ".." + let popped = result.pop(); + assert!(popped); + } + _ => { + result.push(c); + } + } + } + + if result.as_os_str().is_empty() { + ".".into() + } else { + result + } +} + +/// Converts the given `path` to Unix-like path separated by "/". +/// +/// The returned path might not work on Windows if it was canonicalized. On +/// Unix, this function is noop. +pub fn slash_path(path: &Path) -> Cow<'_, Path> { + if cfg!(windows) { + Cow::Owned(to_slash_separated(path).into()) + } else { + Cow::Borrowed(path) + } +} + +fn to_slash_separated(path: &Path) -> OsString { + let mut buf = OsString::with_capacity(path.as_os_str().len()); + let mut components = path.components(); + match components.next() { + Some(c) => buf.push(c), + None => return buf, + } + for c in components { + buf.push("/"); + buf.push(c); + } + buf +} + +/// Persists the temporary file after synchronizing the content. +/// +/// After system crash, the persisted file should have a valid content if +/// existed. However, the persisted file name (or directory entry) could be +/// lost. It's up to caller to synchronize the directory entries. +/// +/// See also for the behavior on Linux. +pub fn persist_temp_file>( + temp_file: NamedTempFile, + new_path: P, +) -> io::Result { + // Ensure persisted file content is flushed to disk. + temp_file.as_file().sync_data()?; + temp_file + .persist(new_path) + .map_err(|PersistError { error, file: _ }| error) +} + +/// Like [`persist_temp_file()`], but doesn't try to overwrite the existing +/// target on Windows. +pub fn persist_content_addressed_temp_file>( + temp_file: NamedTempFile, + new_path: P, +) -> io::Result { + // Ensure new file content is flushed to disk, so the old file content + // wouldn't be lost if existed at the same location. + temp_file.as_file().sync_data()?; + if cfg!(windows) { + // On Windows, overwriting file can fail if the file is opened without + // FILE_SHARE_DELETE for example. We don't need to take a risk if the + // file already exists. + match temp_file.persist_noclobber(&new_path) { + Ok(file) => Ok(file), + Err(PersistError { error, file: _ }) => { + if let Ok(existing_file) = File::open(new_path) { + // TODO: Update mtime to help GC keep this file + Ok(existing_file) + } else { + Err(error) + } + } + } + } else { + // On Unix, rename() is atomic and should succeed even if the + // destination file exists. Checking if the target exists might involve + // non-atomic operation, so don't use persist_noclobber(). + temp_file + .persist(new_path) + .map_err(|PersistError { error, file: _ }| error) + } +} + +/// Opaque value that can be tested to know whether file or directory paths +/// point to the same filesystem entity. +/// +/// The primary use case is to detect file name aliases on case-insensitive +/// filesystem. On Unix, device and inode numbers are compared. +#[derive(Debug, Eq, Hash, PartialEq)] +pub struct FileIdentity(platform::FileIdentity); + +impl FileIdentity { + /// Queries file identity without following symlinks. + /// BUG: On Windows, symbolic links would be followed. + pub fn from_symlink_path(path: impl AsRef) -> io::Result { + platform::file_identity_from_symlink_path(path.as_ref()).map(Self) + } + + /// Queries file identity of the given `file`. + // TODO: do not consume file object + pub fn from_file(file: File) -> io::Result { + platform::file_identity_from_file(file).map(Self) + } +} + +/// Reads from an async source and writes to a sync destination. Does not spawn +/// a task, so writes will block. +pub async fn copy_async_to_sync( + reader: R, + writer: &mut W, +) -> io::Result { + let mut buf = vec![0; 16 << 10]; + let mut total_written_bytes = 0; + + let mut reader = std::pin::pin!(reader); + loop { + let written_bytes = reader.read(&mut buf).await?; + if written_bytes == 0 { + return Ok(total_written_bytes); + } + writer.write_all(&buf[0..written_bytes])?; + total_written_bytes += written_bytes; + } +} + +#[cfg(unix)] +mod platform { + use std::convert::Infallible; + use std::ffi::OsStr; + use std::fs; + use std::fs::File; + use std::io; + use std::os::unix::ffi::OsStrExt as _; + use std::os::unix::fs::MetadataExt as _; + use std::os::unix::fs::PermissionsExt; + use std::os::unix::fs::symlink; + use std::path::Path; + + pub type BadOsStrEncoding = Infallible; + + pub fn os_str_from_bytes(data: &[u8]) -> Result<&OsStr, BadOsStrEncoding> { + Ok(OsStr::from_bytes(data)) + } + + pub fn os_str_to_bytes(data: &OsStr) -> Result<&[u8], BadOsStrEncoding> { + Ok(data.as_bytes()) + } + + /// Whether changing executable bits is permitted on the filesystem of this + /// directory, and whether attempting to flip one has an observable effect. + pub fn check_executable_bit_support(path: impl AsRef) -> io::Result { + // Get current permissions and try to flip just the user's executable bit. + let temp_file = tempfile::tempfile_in(path)?; + let old_mode = temp_file.metadata()?.permissions().mode(); + let new_mode = old_mode ^ 0o100; + let result = temp_file.set_permissions(PermissionsExt::from_mode(new_mode)); + match result { + // If permission was denied, we do not have executable bit support. + Err(err) if err.kind() == io::ErrorKind::PermissionDenied => Ok(false), + Err(err) => Err(err), + Ok(()) => { + // Verify that the permission change was not silently ignored. + let mode = temp_file.metadata()?.permissions().mode(); + Ok(mode == new_mode) + } + } + } + + /// Symlinks are always available on Unix. + pub fn check_symlink_support() -> io::Result { + Ok(true) + } + + /// Creates a new symlink `link` pointing to the `original` path. + /// + /// On Unix, the `original` path doesn't have to be a directory. + pub fn symlink_dir, Q: AsRef>(original: P, link: Q) -> io::Result<()> { + symlink(original, link) + } + + /// Creates a new symlink `link` pointing to the `original` path. + /// + /// On Unix, the `original` path doesn't have to be a file. + pub fn symlink_file, Q: AsRef>(original: P, link: Q) -> io::Result<()> { + symlink(original, link) + } + + #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] + pub struct FileIdentity { + // https://github.com/BurntSushi/same-file/blob/1.0.6/src/unix.rs#L30 + dev: u64, + ino: u64, + } + + impl FileIdentity { + fn from_metadata(metadata: fs::Metadata) -> Self { + Self { + dev: metadata.dev(), + ino: metadata.ino(), + } + } + } + + pub fn file_identity_from_symlink_path(path: &Path) -> io::Result { + path.symlink_metadata().map(FileIdentity::from_metadata) + } + + pub fn file_identity_from_file(file: File) -> io::Result { + file.metadata().map(FileIdentity::from_metadata) + } +} + +#[cfg(windows)] +mod platform { + use std::fs::File; + use std::io; + pub use std::os::windows::fs::symlink_dir; + pub use std::os::windows::fs::symlink_file; + use std::path::Path; + + use winreg::RegKey; + use winreg::enums::HKEY_LOCAL_MACHINE; + + pub use super::fallback::BadOsStrEncoding; + pub use super::fallback::os_str_from_bytes; + pub use super::fallback::os_str_to_bytes; + + /// Symlinks may or may not be enabled on Windows. They require the + /// Developer Mode setting, which is stored in the registry key below. + /// + /// Note: If developer mode is not enabled, the error code of symlink + /// creation will be 1314, `ERROR_PRIVILEGE_NOT_HELD`. + pub fn check_symlink_support() -> io::Result { + let hklm = RegKey::predef(HKEY_LOCAL_MACHINE); + let sideloading = + hklm.open_subkey("SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\AppModelUnlock")?; + let developer_mode: u32 = sideloading.get_value("AllowDevelopmentWithoutDevLicense")?; + Ok(developer_mode == 1) + } + + pub type FileIdentity = same_file::Handle; + + // FIXME: This shouldn't follow symlinks when querying file identity. + // Perhaps, we need to open file with FILE_FLAG_BACKUP_SEMANTICS and + // FILE_FLAG_OPEN_REPARSE_POINT, then pass it to from_file(). Alternatively, + // maybe we can use symlink_metadata(), volume_serial_number(), and + // file_index() when they get stabilized. See the same-file crate and std + // lstat() implementation. https://github.com/rust-lang/rust/issues/63010 + pub fn file_identity_from_symlink_path(path: &Path) -> io::Result { + same_file::Handle::from_path(path) + } + + pub fn file_identity_from_file(file: File) -> io::Result { + same_file::Handle::from_file(file) + } +} + +#[cfg_attr(unix, expect(dead_code))] +mod fallback { + use std::ffi::OsStr; + + use thiserror::Error; + + // Define error per platform so we can explicitly say UTF-8 is expected. + #[derive(Debug, Error)] + #[error("Invalid UTF-8 sequence")] + pub struct BadOsStrEncoding; + + pub fn os_str_from_bytes(data: &[u8]) -> Result<&OsStr, BadOsStrEncoding> { + Ok(str::from_utf8(data).map_err(|_| BadOsStrEncoding)?.as_ref()) + } + + pub fn os_str_to_bytes(data: &OsStr) -> Result<&[u8], BadOsStrEncoding> { + Ok(data.to_str().ok_or(BadOsStrEncoding)?.as_ref()) + } +} + +#[cfg(test)] +mod tests { + use std::io::Write as _; + + use futures::io::Cursor; + use itertools::Itertools as _; + use pollster::FutureExt as _; + use test_case::test_case; + + use super::*; + use crate::tests::TestResult; + use crate::tests::new_temp_dir; + + #[test] + #[cfg(unix)] + fn exec_bit_support_in_temp_dir() -> TestResult { + // Temporary directories on Unix should always have executable support. + // Note that it would be problematic to test in a non-temp directory, as + // a developer's filesystem may or may not have executable bit support. + let dir = new_temp_dir(); + let supported = check_executable_bit_support(dir.path())?; + assert!(supported); + Ok(()) + } + + #[test] + fn test_path_bytes_roundtrip() -> TestResult { + let bytes = b"ascii"; + let path = path_from_bytes(bytes)?; + assert_eq!(path_to_bytes(path)?, bytes); + + let bytes = b"utf-8.\xc3\xa0"; + let path = path_from_bytes(bytes)?; + assert_eq!(path_to_bytes(path)?, bytes); + + let bytes = b"latin1.\xe0"; + if cfg!(unix) { + let path = path_from_bytes(bytes)?; + assert_eq!(path_to_bytes(path)?, bytes); + } else { + assert!(path_from_bytes(bytes).is_err()); + } + Ok(()) + } + + #[test] + fn normalize_too_many_dot_dot() { + assert_eq!(normalize_path(Path::new("foo/..")), Path::new(".")); + assert_eq!(normalize_path(Path::new("foo/../..")), Path::new("..")); + assert_eq!( + normalize_path(Path::new("foo/../../..")), + Path::new("../..") + ); + assert_eq!( + normalize_path(Path::new("foo/../../../bar/baz/..")), + Path::new("../../bar") + ); + } + + #[test] + fn test_slash_path() { + assert_eq!(slash_path(Path::new("")), Path::new("")); + assert_eq!(slash_path(Path::new("foo")), Path::new("foo")); + assert_eq!(slash_path(Path::new("foo/bar")), Path::new("foo/bar")); + assert_eq!(slash_path(Path::new("foo/bar/..")), Path::new("foo/bar/..")); + assert_eq!( + slash_path(Path::new(r"foo\bar")), + if cfg!(windows) { + Path::new("foo/bar") + } else { + Path::new(r"foo\bar") + } + ); + assert_eq!( + slash_path(Path::new(r"..\foo\bar")), + if cfg!(windows) { + Path::new("../foo/bar") + } else { + Path::new(r"..\foo\bar") + } + ); + } + + #[test] + fn test_persist_no_existing_file() -> TestResult { + let temp_dir = new_temp_dir(); + let target = temp_dir.path().join("file"); + let mut temp_file = NamedTempFile::new_in(&temp_dir)?; + temp_file.write_all(b"contents")?; + assert!(persist_content_addressed_temp_file(temp_file, target).is_ok()); + Ok(()) + } + + #[test_case(false ; "existing file open")] + #[test_case(true ; "existing file closed")] + fn test_persist_target_exists(existing_file_closed: bool) -> TestResult { + let temp_dir = new_temp_dir(); + let target = temp_dir.path().join("file"); + let mut temp_file = NamedTempFile::new_in(&temp_dir)?; + temp_file.write_all(b"contents")?; + + let mut file = File::create(&target)?; + file.write_all(b"contents")?; + if existing_file_closed { + drop(file); + } + + assert!(persist_content_addressed_temp_file(temp_file, &target).is_ok()); + Ok(()) + } + + #[test] + fn test_file_identity_hard_link() -> TestResult { + let temp_dir = new_temp_dir(); + let file_path = temp_dir.path().join("file"); + let other_file_path = temp_dir.path().join("other_file"); + let link_path = temp_dir.path().join("link"); + fs::write(&file_path, "")?; + fs::write(&other_file_path, "")?; + fs::hard_link(&file_path, &link_path)?; + assert_eq!( + FileIdentity::from_symlink_path(&file_path)?, + FileIdentity::from_symlink_path(&link_path)? + ); + assert_ne!( + FileIdentity::from_symlink_path(&other_file_path)?, + FileIdentity::from_symlink_path(&link_path)? + ); + assert_eq!( + FileIdentity::from_symlink_path(&file_path)?, + FileIdentity::from_file(File::open(&link_path)?)? + ); + Ok(()) + } + + #[cfg(unix)] + #[test] + fn test_file_identity_unix_symlink_dir() -> TestResult { + let temp_dir = new_temp_dir(); + let dir_path = temp_dir.path().join("dir"); + let symlink_path = temp_dir.path().join("symlink"); + fs::create_dir(&dir_path)?; + std::os::unix::fs::symlink("dir", &symlink_path)?; + // symlink should be identical to itself + assert_eq!( + FileIdentity::from_symlink_path(&symlink_path)?, + FileIdentity::from_symlink_path(&symlink_path)? + ); + // symlink should be different from the target directory + assert_ne!( + FileIdentity::from_symlink_path(&dir_path)?, + FileIdentity::from_symlink_path(&symlink_path)? + ); + // File::open() follows symlinks + assert_eq!( + FileIdentity::from_symlink_path(&dir_path)?, + FileIdentity::from_file(File::open(&symlink_path)?)? + ); + assert_ne!( + FileIdentity::from_symlink_path(&symlink_path)?, + FileIdentity::from_file(File::open(&symlink_path)?)? + ); + Ok(()) + } + + #[cfg(unix)] + #[test] + fn test_file_identity_unix_symlink_loop() -> TestResult { + let temp_dir = new_temp_dir(); + let lower_file_path = temp_dir.path().join("file"); + let upper_file_path = temp_dir.path().join("FILE"); + let lower_symlink_path = temp_dir.path().join("symlink"); + let upper_symlink_path = temp_dir.path().join("SYMLINK"); + fs::write(&lower_file_path, "")?; + std::os::unix::fs::symlink("symlink", &lower_symlink_path)?; + let is_icase_fs = upper_file_path.try_exists()?; + // symlink should be identical to itself + assert_eq!( + FileIdentity::from_symlink_path(&lower_symlink_path)?, + FileIdentity::from_symlink_path(&lower_symlink_path)? + ); + assert_ne!( + FileIdentity::from_symlink_path(&lower_symlink_path)?, + FileIdentity::from_symlink_path(&lower_file_path)? + ); + if is_icase_fs { + assert_eq!( + FileIdentity::from_symlink_path(&lower_symlink_path)?, + FileIdentity::from_symlink_path(&upper_symlink_path)? + ); + } else { + assert!(FileIdentity::from_symlink_path(&upper_symlink_path).is_err()); + } + Ok(()) + } + + #[test] + fn test_copy_async_to_sync_small() -> TestResult { + let input = b"hello"; + let mut output = vec![]; + + let result = copy_async_to_sync(Cursor::new(&input), &mut output).block_on(); + assert!(result.is_ok()); + assert_eq!(result?, 5); + assert_eq!(output, input); + Ok(()) + } + + #[test] + fn test_copy_async_to_sync_large() -> TestResult { + // More than 1 buffer worth of data + let input = (0..100u8).cycle().take(40000).collect_vec(); + let mut output = vec![]; + + let result = copy_async_to_sync(Cursor::new(&input), &mut output).block_on(); + assert!(result.is_ok()); + assert_eq!(result?, 40000); + assert_eq!(output, input); + Ok(()) + } +} diff --git a/lib/src/fileset.pest b/lib/core/src/fileset.pest similarity index 100% rename from lib/src/fileset.pest rename to lib/core/src/fileset.pest diff --git a/lib/core/src/fileset_parser.rs b/lib/core/src/fileset_parser.rs new file mode 100644 index 00000000000..887898491c1 --- /dev/null +++ b/lib/core/src/fileset_parser.rs @@ -0,0 +1,1395 @@ +// Copyright 2024 The Jujutsu Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Parser for the fileset language. + +// TODO: finish documenting fileset.pest +#![allow(missing_docs)] +use std::error; +use std::sync::LazyLock; + +use itertools::Itertools as _; +use pest::Parser as _; +use pest::iterators::Pair; +use pest::pratt_parser::Assoc; +use pest::pratt_parser::Op; +use pest::pratt_parser::PrattParser; +use pest_derive::Parser; +use thiserror::Error; + +use crate::dsl_util; +use crate::dsl_util::AliasDeclaration; +use crate::dsl_util::AliasDeclarationParser; +use crate::dsl_util::AliasDefinitionParser; +use crate::dsl_util::AliasExpandError; +use crate::dsl_util::AliasExpandableExpression; +use crate::dsl_util::AliasId; +use crate::dsl_util::AliasesMap; +use crate::dsl_util::Diagnostics; +use crate::dsl_util::ExpressionFolder; +use crate::dsl_util::FoldableExpression; +use crate::dsl_util::InvalidArguments; +use crate::dsl_util::StringLiteralParser; + +/// A parser for the Fileset language. +#[derive(Parser)] +#[grammar = "fileset.pest"] +struct FilesetParser; + +const STRING_LITERAL_PARSER: StringLiteralParser = StringLiteralParser { + content_rule: Rule::string_content, + escape_rule: Rule::string_escape, +}; + +impl Rule { + fn to_symbol(self) -> Option<&'static str> { + match self { + Self::EOI => None, + Self::whitespace => None, + Self::identifier => None, + Self::strict_identifier_part => None, + Self::strict_identifier => None, + Self::bare_string => None, + Self::string_escape => None, + Self::string_content_char => None, + Self::string_content => None, + Self::string_literal => None, + Self::raw_string_content => None, + Self::raw_string_literal => None, + Self::pattern_kind_op => Some(":"), + Self::negate_op => Some("~"), + Self::union_op => Some("|"), + Self::intersection_op => Some("&"), + Self::difference_op => Some("~"), + Self::prefix_ops => None, + Self::infix_ops => None, + Self::function => None, + Self::function_name => None, + Self::function_arguments => None, + Self::formal_parameters => None, + Self::pattern => None, + Self::bare_string_pattern => None, + Self::primary => None, + Self::expression => None, + Self::program => None, + Self::program_or_bare_string => None, + Self::function_alias_declaration => None, + Self::pattern_alias_declaration => None, + Self::alias_declaration => None, + } + } +} + +/// Manages diagnostic messages emitted during fileset parsing and name +/// resolution. +pub type FilesetDiagnostics = Diagnostics; + +/// Result of fileset parsing and name resolution. +pub type FilesetParseResult = Result; + +/// Error occurred during fileset parsing and name resolution. +#[derive(Debug, Error)] +#[error("{pest_error}")] +pub struct FilesetParseError { + kind: FilesetParseErrorKind, + pest_error: Box>, + source: Option>, +} + +/// Categories of fileset parsing and name resolution error. +#[derive(Clone, Debug, Eq, Error, PartialEq)] +pub enum FilesetParseErrorKind { + /// A syntax error occurred. + #[error("Syntax error")] + SyntaxError, + /// No function for the given name exists. + #[error("Function `{name}` doesn't exist")] + NoSuchFunction { + /// The name of the passed function. + name: String, + /// Potential candidates matching the given name. + candidates: Vec, + }, + /// A fileset function received invalid arguments. + #[error("Function `{name}`: {message}")] + InvalidArguments { + /// The name of the function. + name: String, + /// An additional message to provide more context. + message: String, + }, + /// A redefinition of a function parameter was detected. + #[error("Redefinition of function parameter")] + RedefinedFunctionParameter, + /// An erroneous expression was encountered. + #[error("{0}")] + Expression(String), + /// A fileset expression failed to expand correctly. + #[error("In alias `{0}`")] + InAliasExpansion(String), + /// A function parameter failed to expand correctly. + #[error("In function parameter `{0}`")] + InParameterExpansion(String), + /// An Alias expanded recursively. + #[error("Alias `{0}` expanded recursively")] + RecursiveAlias(String), +} + +impl FilesetParseError { + /// Create a new `FilesetParseError` from with the given `kind` and `span`. + pub fn new(kind: FilesetParseErrorKind, span: pest::Span<'_>) -> Self { + let message = kind.to_string(); + let pest_error = Box::new(pest::error::Error::new_from_span( + pest::error::ErrorVariant::CustomError { message }, + span, + )); + Self { + kind, + pest_error, + source: None, + } + } + + /// Add an additional error source to the `FilesetParseError`. + pub fn with_source(mut self, source: impl Into>) -> Self { + self.source = Some(source.into()); + self + } + + /// Some other expression error. + pub fn expression(message: impl Into, span: pest::Span<'_>) -> Self { + Self::new(FilesetParseErrorKind::Expression(message.into()), span) + } + + /// Category of the underlying error. + pub fn kind(&self) -> &FilesetParseErrorKind { + &self.kind + } +} + +impl AliasExpandError for FilesetParseError { + fn invalid_arguments(err: InvalidArguments<'_>) -> Self { + err.into() + } + + fn recursive_expansion(id: AliasId<'_>, span: pest::Span<'_>) -> Self { + Self::new(FilesetParseErrorKind::RecursiveAlias(id.to_string()), span) + } + + fn within_alias_expansion(self, id: AliasId<'_>, span: pest::Span<'_>) -> Self { + let kind = match id { + AliasId::Symbol(_) | AliasId::Pattern(..) | AliasId::Function(..) => { + FilesetParseErrorKind::InAliasExpansion(id.to_string()) + } + AliasId::Parameter(_) => FilesetParseErrorKind::InParameterExpansion(id.to_string()), + }; + Self::new(kind, span).with_source(self) + } +} + +impl From> for FilesetParseError { + fn from(err: pest::error::Error) -> Self { + Self { + kind: FilesetParseErrorKind::SyntaxError, + pest_error: Box::new(rename_rules_in_pest_error(err)), + source: None, + } + } +} + +impl From> for FilesetParseError { + fn from(err: InvalidArguments<'_>) -> Self { + let kind = FilesetParseErrorKind::InvalidArguments { + name: err.name.to_owned(), + message: err.message, + }; + Self::new(kind, err.span) + } +} + +fn rename_rules_in_pest_error(err: pest::error::Error) -> pest::error::Error { + err.renamed_rules(|rule| { + rule.to_symbol() + .map(|sym| format!("`{sym}`")) + .unwrap_or_else(|| format!("<{rule:?}>")) + }) +} + +/// Describes the types of Expressions the fileset language supports. +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum ExpressionKind<'i> { + /// A identifier + Identifier(&'i str), + /// A String. + String(String), + /// `:` where `` is usually `Identifier` or `String`. + Pattern(Box>), + /// A urnary expression. + Unary(UnaryOp, Box>), + /// A binary expression with both of its nodes. + Binary(BinaryOp, Box>, Box>), + /// `x | y | ..` + UnionAll(Vec>), + /// A function call. + FunctionCall(Box>), + /// Identity node to preserve the span in the source text. + AliasExpanded(AliasId<'i>, Box>), +} + +impl<'i> FoldableExpression<'i> for ExpressionKind<'i> { + fn fold(self, folder: &mut F, span: pest::Span<'i>) -> Result + where + F: ExpressionFolder<'i, Self> + ?Sized, + { + match self { + Self::Identifier(name) => folder.fold_identifier(name, span), + Self::String(_) => Ok(self), + Self::Pattern(pattern) => folder.fold_pattern(pattern, span), + Self::Unary(op, arg) => { + let arg = Box::new(folder.fold_expression(*arg)?); + Ok(Self::Unary(op, arg)) + } + Self::Binary(op, lhs, rhs) => { + let lhs = Box::new(folder.fold_expression(*lhs)?); + let rhs = Box::new(folder.fold_expression(*rhs)?); + Ok(Self::Binary(op, lhs, rhs)) + } + Self::UnionAll(nodes) => { + let nodes = dsl_util::fold_expression_nodes(folder, nodes)?; + Ok(Self::UnionAll(nodes)) + } + Self::FunctionCall(function) => folder.fold_function_call(function, span), + Self::AliasExpanded(id, subst) => { + let subst = Box::new(folder.fold_expression(*subst)?); + Ok(Self::AliasExpanded(id, subst)) + } + } + } +} + +impl<'i> AliasExpandableExpression<'i> for ExpressionKind<'i> { + fn identifier(name: &'i str) -> Self { + Self::Identifier(name) + } + + fn pattern(pattern: Box>) -> Self { + Self::Pattern(pattern) + } + + fn function_call(function: Box>) -> Self { + Self::FunctionCall(function) + } + + fn alias_expanded(id: AliasId<'i>, subst: Box>) -> Self { + Self::AliasExpanded(id, subst) + } +} + +/// A urnary operation in the Fileset language. +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +pub enum UnaryOp { + /// `~` + Negate, +} + +/// A binary operation in the Fileset language. +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +pub enum BinaryOp { + /// `&` + Intersection, + /// `~` + Difference, +} + +/// A Expression in the Fileset language. +pub type ExpressionNode<'i> = dsl_util::ExpressionNode<'i, ExpressionKind<'i>>; +/// A FunctionCall in the Fileset language. +pub type FunctionCallNode<'i> = dsl_util::FunctionCallNode<'i, ExpressionKind<'i>>; +/// A Pattern in the Fileset language. +pub type PatternNode<'i> = dsl_util::PatternNode<'i, ExpressionKind<'i>>; + +fn union_nodes<'i>(lhs: ExpressionNode<'i>, rhs: ExpressionNode<'i>) -> ExpressionNode<'i> { + let span = lhs.span.start_pos().span(&rhs.span.end_pos()); + let expr = match lhs.kind { + // Flatten "x | y | z" to save recursion stack. Machine-generated query + // might have long chain of unions. + ExpressionKind::UnionAll(mut nodes) => { + nodes.push(rhs); + ExpressionKind::UnionAll(nodes) + } + _ => ExpressionKind::UnionAll(vec![lhs, rhs]), + }; + ExpressionNode::new(expr, span) +} + +fn parse_function_call_node(pair: Pair) -> FilesetParseResult { + assert_eq!(pair.as_rule(), Rule::function); + let [name_pair, args_pair] = pair.into_inner().collect_array().unwrap(); + assert_eq!(name_pair.as_rule(), Rule::function_name); + assert_eq!(args_pair.as_rule(), Rule::function_arguments); + let name_span = name_pair.as_span(); + let args_span = args_pair.as_span(); + let name = name_pair.as_str(); + let args = args_pair + .into_inner() + .map(parse_expression_node) + .try_collect()?; + Ok(FunctionCallNode { + name, + name_span, + args, + keyword_args: vec![], // unsupported + args_span, + }) +} + +fn parse_as_string_literal(pair: Pair) -> String { + match pair.as_rule() { + Rule::identifier => pair.as_str().to_owned(), + Rule::string_literal => STRING_LITERAL_PARSER.parse(pair.into_inner()), + Rule::raw_string_literal => { + let [content] = pair.into_inner().collect_array().unwrap(); + assert_eq!(content.as_rule(), Rule::raw_string_content); + content.as_str().to_owned() + } + r => panic!("unexpected string literal rule: {r:?}"), + } +} + +fn parse_primary_node(pair: Pair) -> FilesetParseResult { + assert_eq!(pair.as_rule(), Rule::primary); + let span = pair.as_span(); + let first = pair.into_inner().next().unwrap(); + let expr = match first.as_rule() { + // Ignore inner span to preserve parenthesized expression as such. + Rule::expression => parse_expression_node(first)?.kind, + Rule::function => { + let function = Box::new(parse_function_call_node(first)?); + ExpressionKind::FunctionCall(function) + } + Rule::pattern => { + let [lhs, op, rhs] = first.into_inner().collect_array().unwrap(); + assert_eq!(lhs.as_rule(), Rule::strict_identifier); + assert_eq!(op.as_rule(), Rule::pattern_kind_op); + let pattern = Box::new(PatternNode { + name: lhs.as_str(), + name_span: lhs.as_span(), + value: parse_primary_node(rhs)?, + }); + ExpressionKind::Pattern(pattern) + } + Rule::identifier => ExpressionKind::Identifier(first.as_str()), + Rule::string_literal | Rule::raw_string_literal => { + ExpressionKind::String(parse_as_string_literal(first)) + } + r => panic!("unexpected primary rule: {r:?}"), + }; + Ok(ExpressionNode::new(expr, span)) +} + +fn parse_expression_node(pair: Pair) -> FilesetParseResult { + assert_eq!(pair.as_rule(), Rule::expression); + static PRATT: LazyLock> = LazyLock::new(|| { + PrattParser::new() + .op(Op::infix(Rule::union_op, Assoc::Left)) + .op(Op::infix(Rule::intersection_op, Assoc::Left) + | Op::infix(Rule::difference_op, Assoc::Left)) + .op(Op::prefix(Rule::negate_op)) + }); + PRATT + .map_primary(parse_primary_node) + .map_prefix(|op, rhs| { + let op_kind = match op.as_rule() { + Rule::negate_op => UnaryOp::Negate, + r => panic!("unexpected prefix operator rule {r:?}"), + }; + let rhs = Box::new(rhs?); + let span = op.as_span().start_pos().span(&rhs.span.end_pos()); + let expr = ExpressionKind::Unary(op_kind, rhs); + Ok(ExpressionNode::new(expr, span)) + }) + .map_infix(|lhs, op, rhs| { + let op_kind = match op.as_rule() { + Rule::union_op => return Ok(union_nodes(lhs?, rhs?)), + Rule::intersection_op => BinaryOp::Intersection, + Rule::difference_op => BinaryOp::Difference, + r => panic!("unexpected infix operator rule {r:?}"), + }; + let lhs = Box::new(lhs?); + let rhs = Box::new(rhs?); + let span = lhs.span.start_pos().span(&rhs.span.end_pos()); + let expr = ExpressionKind::Binary(op_kind, lhs, rhs); + Ok(ExpressionNode::new(expr, span)) + }) + .parse(pair.into_inner()) +} + +/// Parses text into expression tree. No name resolution is made at this stage. +pub fn parse_program(text: &str) -> FilesetParseResult> { + let mut pairs = FilesetParser::parse(Rule::program, text)?; + let first = pairs.next().unwrap(); + parse_expression_node(first) +} + +/// Parses text into expression tree with bare string fallback. No name +/// resolution is made at this stage. +/// +/// If the text can't be parsed as a fileset expression, and if it doesn't +/// contain any operator-like characters, it will be parsed as a file path. +pub fn parse_program_or_bare_string(text: &str) -> FilesetParseResult> { + let mut pairs = FilesetParser::parse(Rule::program_or_bare_string, text)?; + let first = pairs.next().unwrap(); + let span = first.as_span(); + let expr = match first.as_rule() { + Rule::expression => return parse_expression_node(first), + Rule::bare_string_pattern => { + let [lhs, op, rhs] = first.into_inner().collect_array().unwrap(); + assert_eq!(lhs.as_rule(), Rule::strict_identifier); + assert_eq!(op.as_rule(), Rule::pattern_kind_op); + assert_eq!(rhs.as_rule(), Rule::bare_string); + let name_span = lhs.as_span(); + let value_span = rhs.as_span(); + let name = lhs.as_str(); + let value_expr = ExpressionKind::String(rhs.as_str().to_owned()); + let pattern = Box::new(PatternNode { + name, + name_span, + value: ExpressionNode::new(value_expr, value_span), + }); + ExpressionKind::Pattern(pattern) + } + Rule::bare_string => ExpressionKind::String(first.as_str().to_owned()), + r => panic!("unexpected program or bare string rule: {r:?}"), + }; + Ok(ExpressionNode::new(expr, span)) +} + +/// Map of fileset aliases. +pub type FilesetAliasesMap = AliasesMap; + +/// A FilesetAliasesParser is responsible for parsing String expressions into +/// Fileset Aliases. +#[derive(Clone, Debug, Default)] +pub struct FilesetAliasParser; + +impl AliasDeclarationParser for FilesetAliasParser { + type Error = FilesetParseError; + + fn parse_declaration(&self, source: &str) -> Result { + let mut pairs = FilesetParser::parse(Rule::alias_declaration, source)?; + let first = pairs.next().unwrap(); + match first.as_rule() { + Rule::strict_identifier => Ok(AliasDeclaration::Symbol(first.as_str().to_owned())), + Rule::pattern_alias_declaration => { + let [name_pair, op, param_pair] = first.into_inner().collect_array().unwrap(); + assert_eq!(name_pair.as_rule(), Rule::strict_identifier); + assert_eq!(op.as_rule(), Rule::pattern_kind_op); + assert_eq!(param_pair.as_rule(), Rule::strict_identifier); + let name = name_pair.as_str().to_owned(); + let param = param_pair.as_str().to_owned(); + Ok(AliasDeclaration::Pattern(name, param)) + } + Rule::function_alias_declaration => { + let [name_pair, params_pair] = first.into_inner().collect_array().unwrap(); + assert_eq!(name_pair.as_rule(), Rule::function_name); + assert_eq!(params_pair.as_rule(), Rule::formal_parameters); + let name = name_pair.as_str().to_owned(); + let params_span = params_pair.as_span(); + let params = params_pair + .into_inner() + .map(|pair| match pair.as_rule() { + Rule::strict_identifier => pair.as_str().to_owned(), + r => panic!("unexpected formal parameter rule {r:?}"), + }) + .collect_vec(); + if params.iter().all_unique() { + Ok(AliasDeclaration::Function(name, params)) + } else { + Err(FilesetParseError::new( + FilesetParseErrorKind::RedefinedFunctionParameter, + params_span, + )) + } + } + r => panic!("unexpected alias declaration rule {r:?}"), + } + } +} + +impl AliasDefinitionParser for FilesetAliasParser { + type Output<'i> = ExpressionKind<'i>; + type Error = FilesetParseError; + + fn parse_definition<'i>(&self, source: &'i str) -> Result, Self::Error> { + parse_program(source) + } +} + +/// Expand the aliases in `node` and `aliases_map`. +pub fn expand_aliases<'i>( + node: ExpressionNode<'i>, + aliases_map: &'i FilesetAliasesMap, +) -> FilesetParseResult> { + dsl_util::expand_aliases(node, aliases_map) +} + +/// Expect a String literal of `type_name` in `node`. +pub fn expect_string_literal<'a>( + type_name: &str, + node: &'a ExpressionNode<'_>, +) -> FilesetParseResult<&'a str> { + catch_aliases_no_diagnostics(node, |node| match &node.kind { + ExpressionKind::Identifier(name) => Ok(*name), + ExpressionKind::String(name) => Ok(name), + _ => Err(FilesetParseError::expression( + format!("Expected {type_name}"), + node.span, + )), + }) +} + +/// Applies the given function to the innermost `node` by unwrapping alias +/// expansion nodes. Appends alias expansion stack to error and diagnostics. +pub fn catch_aliases<'a, 'i, T>( + diagnostics: &mut FilesetDiagnostics, + node: &'a ExpressionNode<'i>, + f: impl FnOnce(&mut FilesetDiagnostics, &'a ExpressionNode<'i>) -> Result, +) -> Result { + let (node, stack) = skip_aliases(node); + if stack.is_empty() { + f(diagnostics, node) + } else { + let mut inner_diagnostics = FilesetDiagnostics::new(); + let result = f(&mut inner_diagnostics, node); + diagnostics.extend_with(inner_diagnostics, |diag| attach_aliases_err(diag, &stack)); + result.map_err(|err| attach_aliases_err(err, &stack)) + } +} + +fn catch_aliases_no_diagnostics<'a, 'i, T>( + node: &'a ExpressionNode<'i>, + f: impl FnOnce(&'a ExpressionNode<'i>) -> Result, +) -> Result { + let (node, stack) = skip_aliases(node); + f(node).map_err(|err| attach_aliases_err(err, &stack)) +} + +fn skip_aliases<'a, 'i>( + mut node: &'a ExpressionNode<'i>, +) -> (&'a ExpressionNode<'i>, Vec<(AliasId<'i>, pest::Span<'i>)>) { + let mut stack = Vec::new(); + while let ExpressionKind::AliasExpanded(id, subst) = &node.kind { + stack.push((*id, node.span)); + node = subst; + } + (node, stack) +} + +fn attach_aliases_err( + err: FilesetParseError, + stack: &[(AliasId<'_>, pest::Span<'_>)], +) -> FilesetParseError { + stack + .iter() + .rfold(err, |err, &(id, span)| err.within_alias_expansion(id, span)) +} + +#[cfg(test)] +mod tests { + use assert_matches::assert_matches; + + use super::*; + use crate::dsl_util::KeywordArgument; + use crate::tests::TestResult; + + #[derive(Debug)] + struct WithFilesetAliasesMap { + aliases_map: FilesetAliasesMap, + } + + impl WithFilesetAliasesMap { + fn parse<'i>(&'i self, text: &'i str) -> FilesetParseResult> { + let node = parse_program(text)?; + expand_aliases(node, &self.aliases_map) + } + + fn parse_normalized<'i>(&'i self, text: &'i str) -> ExpressionNode<'i> { + normalize_tree(self.parse(text).unwrap()) + } + } + + fn with_aliases( + aliases: impl IntoIterator, impl Into)>, + ) -> WithFilesetAliasesMap { + let mut aliases_map = FilesetAliasesMap::new(); + for (decl, defn) in aliases { + aliases_map.insert(decl, defn, None).unwrap(); + } + WithFilesetAliasesMap { aliases_map } + } + + fn parse_into_kind(text: &str) -> Result, FilesetParseErrorKind> { + parse_program(text) + .map(|node| node.kind) + .map_err(|err| err.kind) + } + + fn parse_maybe_bare_into_kind(text: &str) -> Result, FilesetParseErrorKind> { + parse_program_or_bare_string(text) + .map(|node| node.kind) + .map_err(|err| err.kind) + } + + fn parse_normalized(text: &str) -> ExpressionNode<'_> { + normalize_tree(parse_program(text).unwrap()) + } + + fn parse_maybe_bare_normalized(text: &str) -> ExpressionNode<'_> { + normalize_tree(parse_program_or_bare_string(text).unwrap()) + } + + /// Drops auxiliary data from parsed tree so it can be compared with other. + fn normalize_tree(node: ExpressionNode) -> ExpressionNode { + fn empty_span() -> pest::Span<'static> { + pest::Span::new("", 0, 0).unwrap() + } + + fn normalize_list(nodes: Vec) -> Vec { + nodes.into_iter().map(normalize_tree).collect() + } + + fn normalize_function_call(function: FunctionCallNode) -> FunctionCallNode { + FunctionCallNode { + name: function.name, + name_span: empty_span(), + args: normalize_list(function.args), + keyword_args: function + .keyword_args + .into_iter() + .map(|arg| KeywordArgument { + name: arg.name, + name_span: empty_span(), + value: normalize_tree(arg.value), + }) + .collect(), + args_span: empty_span(), + } + } + + let normalized_kind = match node.kind { + ExpressionKind::Identifier(_) | ExpressionKind::String(_) => node.kind, + ExpressionKind::Pattern(pattern) => { + let pattern = Box::new(PatternNode { + name: pattern.name, + name_span: empty_span(), + value: normalize_tree(pattern.value), + }); + ExpressionKind::Pattern(pattern) + } + ExpressionKind::Unary(op, arg) => { + let arg = Box::new(normalize_tree(*arg)); + ExpressionKind::Unary(op, arg) + } + ExpressionKind::Binary(op, lhs, rhs) => { + let lhs = Box::new(normalize_tree(*lhs)); + let rhs = Box::new(normalize_tree(*rhs)); + ExpressionKind::Binary(op, lhs, rhs) + } + ExpressionKind::UnionAll(nodes) => { + let nodes = normalize_list(nodes); + ExpressionKind::UnionAll(nodes) + } + ExpressionKind::FunctionCall(function) => { + let function = Box::new(normalize_function_call(*function)); + ExpressionKind::FunctionCall(function) + } + ExpressionKind::AliasExpanded(_, subst) => normalize_tree(*subst).kind, + }; + ExpressionNode { + kind: normalized_kind, + span: empty_span(), + } + } + + #[test] + fn test_parse_tree_eq() { + assert_eq!( + parse_normalized(r#" foo( x ) | ~bar:"baz" "#), + parse_normalized(r#"(foo(x))|(~(bar:"baz"))"#) + ); + assert_ne!(parse_normalized(r#" foo "#), parse_normalized(r#" "foo" "#)); + } + + #[test] + fn test_parse_invalid_function_name() { + assert_eq!( + parse_into_kind("5foo(x)"), + Err(FilesetParseErrorKind::SyntaxError) + ); + } + + #[test] + fn test_parse_whitespace() { + let ascii_whitespaces: String = ('\x00'..='\x7f') + .filter(char::is_ascii_whitespace) + .collect(); + assert_eq!( + parse_normalized(&format!("{ascii_whitespaces}f()")), + parse_normalized("f()") + ); + } + + #[test] + fn test_parse_identifier() { + assert_eq!( + parse_into_kind("dir/foo-bar_0.baz"), + Ok(ExpressionKind::Identifier("dir/foo-bar_0.baz")) + ); + assert_eq!( + parse_into_kind("cli-reference@.md.snap"), + Ok(ExpressionKind::Identifier("cli-reference@.md.snap")) + ); + assert_eq!( + parse_into_kind("柔術.jj"), + Ok(ExpressionKind::Identifier("柔術.jj")) + ); + assert_eq!( + parse_into_kind(r#"Windows\Path"#), + Ok(ExpressionKind::Identifier(r#"Windows\Path"#)) + ); + assert_eq!( + parse_into_kind("glob*[chars]?"), + Ok(ExpressionKind::Identifier("glob*[chars]?")) + ); + } + + #[test] + fn test_parse_string_literal() { + // "\" escapes + assert_eq!( + parse_into_kind(r#" "\t\r\n\"\\\0\e" "#), + Ok(ExpressionKind::String("\t\r\n\"\\\0\u{1b}".to_owned())), + ); + + // Invalid "\" escape + assert_eq!( + parse_into_kind(r#" "\y" "#), + Err(FilesetParseErrorKind::SyntaxError), + ); + + // Single-quoted raw string + assert_eq!( + parse_into_kind(r#" '' "#), + Ok(ExpressionKind::String("".to_owned())), + ); + assert_eq!( + parse_into_kind(r#" 'a\n' "#), + Ok(ExpressionKind::String(r"a\n".to_owned())), + ); + assert_eq!( + parse_into_kind(r#" '\' "#), + Ok(ExpressionKind::String(r"\".to_owned())), + ); + assert_eq!( + parse_into_kind(r#" '"' "#), + Ok(ExpressionKind::String(r#"""#.to_owned())), + ); + + // Hex bytes + assert_eq!( + parse_into_kind(r#""\x61\x65\x69\x6f\x75""#), + Ok(ExpressionKind::String("aeiou".to_owned())), + ); + assert_eq!( + parse_into_kind(r#""\xe0\xe8\xec\xf0\xf9""#), + Ok(ExpressionKind::String("àèìðù".to_owned())), + ); + assert_eq!( + parse_into_kind(r#""\x""#), + Err(FilesetParseErrorKind::SyntaxError), + ); + assert_eq!( + parse_into_kind(r#""\xf""#), + Err(FilesetParseErrorKind::SyntaxError), + ); + assert_eq!( + parse_into_kind(r#""\xgg""#), + Err(FilesetParseErrorKind::SyntaxError), + ); + } + + #[test] + fn test_parse_pattern() -> TestResult { + fn unwrap_pattern(kind: ExpressionKind<'_>) -> (&str, ExpressionKind<'_>) { + match kind { + ExpressionKind::Pattern(pattern) => (pattern.name, pattern.value.kind), + _ => panic!("unexpected expression: {kind:?}"), + } + } + + assert_eq!( + unwrap_pattern(parse_into_kind(r#" foo:bar "#)?), + ("foo", ExpressionKind::Identifier("bar")) + ); + assert_eq!( + unwrap_pattern(parse_into_kind(" foo:glob*[chars]? ")?), + ("foo", ExpressionKind::Identifier("glob*[chars]?")) + ); + assert_eq!( + unwrap_pattern(parse_into_kind(r#" foo:"bar" "#)?), + ("foo", ExpressionKind::String("bar".to_owned())) + ); + assert_eq!( + unwrap_pattern(parse_into_kind(r#" foo:"" "#)?), + ("foo", ExpressionKind::String("".to_owned())) + ); + assert_eq!( + unwrap_pattern(parse_into_kind(r#" foo:'\' "#)?), + ("foo", ExpressionKind::String(r"\".to_owned())) + ); + assert_eq!( + parse_into_kind(r#" foo: "#), + Err(FilesetParseErrorKind::SyntaxError) + ); + + // Whitespace isn't allowed in between + assert_eq!( + parse_into_kind(r#" foo: "" "#), + Err(FilesetParseErrorKind::SyntaxError) + ); + assert_eq!( + parse_into_kind(r#" foo :"" "#), + Err(FilesetParseErrorKind::SyntaxError) + ); + // Whitespace is allowed in parenthesized value expression + assert_eq!( + parse_normalized("foo:( 'bar' )"), + parse_normalized("foo:'bar'") + ); + + // Functions are allowed + assert_eq!(parse_normalized("x:f(y)"), parse_normalized("x:(f(y))")); + // Logical operators have lower binding strength + assert_eq!(parse_normalized("x:y&z"), parse_normalized("(x:y)&(z)")); + assert_matches!( + parse_into_kind("x:~y"), // (x:) ~ (y) + Err(FilesetParseErrorKind::SyntaxError) + ); + + // Pattern prefix is like (type)x cast, so is evaluated from right + assert_eq!(parse_normalized("x:y:z"), parse_normalized("x:(y:z)")); + Ok(()) + } + + #[test] + fn test_parse_operator() -> TestResult { + assert_matches!( + parse_into_kind("~x"), + Ok(ExpressionKind::Unary(UnaryOp::Negate, _)) + ); + assert_matches!( + parse_into_kind("x|y"), + Ok(ExpressionKind::UnionAll(nodes)) if nodes.len() == 2 + ); + assert_matches!( + parse_into_kind("x|y|z"), + Ok(ExpressionKind::UnionAll(nodes)) if nodes.len() == 3 + ); + assert_matches!( + parse_into_kind("x&y"), + Ok(ExpressionKind::Binary(BinaryOp::Intersection, _, _)) + ); + assert_matches!( + parse_into_kind("x~y"), + Ok(ExpressionKind::Binary(BinaryOp::Difference, _, _)) + ); + + // Set operator associativity/precedence + assert_eq!(parse_normalized("~x|y"), parse_normalized("(~x)|y")); + assert_eq!(parse_normalized("x&~y"), parse_normalized("x&(~y)")); + assert_eq!(parse_normalized("x~~y"), parse_normalized("x~(~y)")); + assert_eq!(parse_normalized("x~~~y"), parse_normalized("x~(~(~y))")); + assert_eq!(parse_normalized("x|y|z"), parse_normalized("(x|y)|z")); + assert_eq!(parse_normalized("x&y|z"), parse_normalized("(x&y)|z")); + assert_eq!(parse_normalized("x|y&z"), parse_normalized("x|(y&z)")); + assert_eq!(parse_normalized("x|y~z"), parse_normalized("x|(y~z)")); + assert_eq!(parse_normalized("~x:y"), parse_normalized("~(x:y)")); + assert_eq!(parse_normalized("x|y:z"), parse_normalized("x|(y:z)")); + + // Expression span + assert_eq!(parse_program(" ~ x ")?.span.as_str(), "~ x"); + assert_eq!(parse_program(" x |y ")?.span.as_str(), "x |y"); + assert_eq!(parse_program(" (x) ")?.span.as_str(), "(x)"); + assert_eq!(parse_program("~( x|y) ")?.span.as_str(), "~( x|y)"); + Ok(()) + } + + #[test] + fn test_parse_function_call() -> TestResult { + fn unwrap_function_call(node: ExpressionNode<'_>) -> Box> { + match node.kind { + ExpressionKind::FunctionCall(function) => function, + _ => panic!("unexpected expression: {node:?}"), + } + } + + assert_matches!( + parse_into_kind("foo()"), + Ok(ExpressionKind::FunctionCall(_)) + ); + + // Trailing comma isn't allowed for empty argument + assert!(parse_into_kind("foo(,)").is_err()); + + // Trailing comma is allowed for the last argument + assert_eq!(parse_normalized("foo(a,)"), parse_normalized("foo(a)")); + assert_eq!(parse_normalized("foo(a , )"), parse_normalized("foo(a)")); + assert!(parse_into_kind("foo(,a)").is_err()); + assert!(parse_into_kind("foo(a,,)").is_err()); + assert!(parse_into_kind("foo(a , , )").is_err()); + assert_eq!(parse_normalized("foo(a,b,)"), parse_normalized("foo(a,b)")); + assert!(parse_into_kind("foo(a,,b)").is_err()); + + // Expression span + let function = unwrap_function_call(parse_program("foo( a, (b) , ~(c) )")?); + assert_eq!(function.name_span.as_str(), "foo"); + assert_eq!(function.args_span.as_str(), "a, (b) , ~(c)"); + assert_eq!(function.args[0].span.as_str(), "a"); + assert_eq!(function.args[1].span.as_str(), "(b)"); + assert_eq!(function.args[2].span.as_str(), "~(c)"); + Ok(()) + } + + #[test] + fn test_parse_bare_string() -> TestResult { + fn unwrap_pattern(kind: ExpressionKind<'_>) -> (&str, ExpressionKind<'_>) { + match kind { + ExpressionKind::Pattern(pattern) => (pattern.name, pattern.value.kind), + _ => panic!("unexpected expression: {kind:?}"), + } + } + + // Valid expression should be parsed as such + assert_eq!( + parse_maybe_bare_into_kind(" valid "), + Ok(ExpressionKind::Identifier("valid")) + ); + assert_eq!( + parse_maybe_bare_normalized("f(x)&y"), + parse_normalized("f(x)&y") + ); + assert_eq!( + unwrap_pattern(parse_maybe_bare_into_kind("foo:bar")?), + ("foo", ExpressionKind::Identifier("bar")) + ); + + // Bare string + assert_eq!( + parse_maybe_bare_into_kind("Foo Bar.txt"), + Ok(ExpressionKind::String("Foo Bar.txt".to_owned())) + ); + assert_eq!( + parse_maybe_bare_into_kind(r#"Windows\Path with space"#), + Ok(ExpressionKind::String( + r#"Windows\Path with space"#.to_owned() + )) + ); + assert_eq!( + parse_maybe_bare_into_kind("柔 術 . j j"), + Ok(ExpressionKind::String("柔 術 . j j".to_owned())) + ); + assert_eq!( + parse_maybe_bare_into_kind("Unicode emoji 💩"), + Ok(ExpressionKind::String("Unicode emoji 💩".to_owned())) + ); + assert_eq!( + parse_maybe_bare_into_kind("looks like & expression"), + Err(FilesetParseErrorKind::SyntaxError) + ); + assert_eq!( + parse_maybe_bare_into_kind("unbalanced_parens("), + Err(FilesetParseErrorKind::SyntaxError) + ); + + // Bare string pattern + assert_eq!( + unwrap_pattern(parse_maybe_bare_into_kind("foo: bar baz")?), + ("foo", ExpressionKind::String(" bar baz".to_owned())) + ); + assert_eq!( + unwrap_pattern(parse_maybe_bare_into_kind("foo:glob * [chars]?")?), + ("foo", ExpressionKind::String("glob * [chars]?".to_owned())) + ); + assert_eq!( + parse_maybe_bare_into_kind("foo: bar:baz"), + Err(FilesetParseErrorKind::SyntaxError) + ); + assert_eq!( + parse_maybe_bare_into_kind("foo:"), + Err(FilesetParseErrorKind::SyntaxError) + ); + assert_eq!( + parse_maybe_bare_into_kind(r#"foo:"unclosed quote"#), + Err(FilesetParseErrorKind::SyntaxError) + ); + + // Surrounding spaces are simply preserved. They could be trimmed, but + // space is valid bare_string character. + assert_eq!( + parse_maybe_bare_into_kind(" No trim "), + Ok(ExpressionKind::String(" No trim ".to_owned())) + ); + Ok(()) + } + + #[test] + fn test_parse_error() { + insta::assert_snapshot!(parse_program("foo|").unwrap_err().to_string(), @" + --> 1:5 + | + 1 | foo| + | ^--- + | + = expected `~` or + "); + } + + #[test] + fn test_parse_alias_symbol_decl() -> TestResult { + let mut aliases_map = FilesetAliasesMap::new(); + aliases_map.insert("sym", "symbol", None)?; + assert_eq!(aliases_map.symbol_names().count(), 1); + let (id, defn, _doc) = aliases_map.get_symbol("sym").unwrap(); + assert_eq!(id, AliasId::Symbol("sym")); + assert_eq!(defn, "symbol"); + + // Non-ASCII character isn't allowed in alias symbol. This rule can be + // relaxed if needed. + assert!(aliases_map.insert("柔術", "none()", None).is_err()); + Ok(()) + } + + #[test] + fn test_parse_alias_pattern_decl() -> TestResult { + let mut aliases_map = FilesetAliasesMap::new(); + assert!(aliases_map.insert("pat:", "bad_pattern", None).is_err()); + aliases_map.insert("pat:a", "pattern_a", None)?; + aliases_map.insert("pat:b", "pattern_b", None)?; + assert_eq!(aliases_map.pattern_names().count(), 1); + let (id, param, defn, _doc) = aliases_map.get_pattern("pat").unwrap(); + assert_eq!(id, AliasId::Pattern("pat", "b")); + assert_eq!(param, "b"); + assert_eq!(defn, "pattern_b"); + + // Non-ASCII character isn't allowed. This rule can be relaxed if + // needed. + assert!(aliases_map.insert("柔術:x", "none()", None).is_err()); + assert!(aliases_map.insert("x:柔術", "none()", None).is_err()); + Ok(()) + } + + #[test] + fn test_parse_alias_func_decl() -> TestResult { + let mut aliases_map = FilesetAliasesMap::new(); + assert!(aliases_map.insert("5func()", "bad_function", None).is_err()); + aliases_map.insert("func()", "function_0", None)?; + aliases_map.insert("func(a)", "function_1a", None)?; + aliases_map.insert("func(b)", "function_1b", None)?; + aliases_map.insert("func(a, b)", "function_2", None)?; + assert_eq!(aliases_map.function_names().count(), 1); + + let (id, params, defn, _doc) = aliases_map.get_function("func", 0).unwrap(); + assert_eq!(id, AliasId::Function("func", &[])); + assert!(params.is_empty()); + assert_eq!(defn, "function_0"); + + let (id, params, defn, _doc) = aliases_map.get_function("func", 1).unwrap(); + assert_eq!(id, AliasId::Function("func", &["b".to_owned()])); + assert_eq!(params, ["b"]); + assert_eq!(defn, "function_1b"); + + let (id, params, defn, _doc) = aliases_map.get_function("func", 2).unwrap(); + assert_eq!( + id, + AliasId::Function("func", &["a".to_owned(), "b".to_owned()]) + ); + assert_eq!(params, ["a", "b"]); + assert_eq!(defn, "function_2"); + + assert!(aliases_map.get_function("func", 3).is_none()); + Ok(()) + } + + #[test] + fn test_parse_alias_formal_parameter() { + let mut aliases_map = FilesetAliasesMap::new(); + // Formal parameter 'a' can't be redefined + assert_eq!( + aliases_map.insert("f(a, a)", "bad", None).unwrap_err().kind, + FilesetParseErrorKind::RedefinedFunctionParameter + ); + // Trailing comma isn't allowed for empty parameter + assert!(aliases_map.insert("f(,)", "bad", None).is_err()); + // Trailing comma is allowed for the last parameter + assert!(aliases_map.insert("g(a,)", "bad", None).is_ok()); + assert!(aliases_map.insert("h(a , )", "bad", None).is_ok()); + assert!(aliases_map.insert("i(,a)", "bad", None).is_err()); + assert!(aliases_map.insert("j(a,,)", "bad", None).is_err()); + assert!(aliases_map.insert("k(a , , )", "bad", None).is_err()); + assert!(aliases_map.insert("l(a,b,)", "bad", None).is_ok()); + assert!(aliases_map.insert("m(a,,b)", "bad", None).is_err()); + } + + #[test] + fn test_expand_symbol_alias() { + assert_eq!( + with_aliases([("AB", "a&b")]).parse_normalized("AB|c"), + parse_normalized("(a&b)|c") + ); + assert_eq!( + with_aliases([("AB", "a|b")]).parse_normalized("AB~f(AB)"), + parse_normalized("(a|b)~f(a|b)") + ); + + // Not string substitution 'a&b|c', but tree substitution. + assert_eq!( + with_aliases([("BC", "b|c")]).parse_normalized("a&BC"), + parse_normalized("a&(b|c)") + ); + + // String literal should not be substituted with alias. + assert_eq!( + with_aliases([("A", "a")]).parse_normalized(r#"A|"A"|'A'"#), + parse_normalized("a|'A'|'A'") + ); + + // Kind of string pattern should not be substituted, which is similar to + // function name. + assert_eq!( + with_aliases([("A", "a")]).parse_normalized("A:b"), + parse_normalized("A:b") + ); + + // Value of string pattern can be substituted if it's an identifier. + assert_eq!( + with_aliases([("A", "a")]).parse_normalized("p:A"), + parse_normalized("p:a") + ); + assert_eq!( + with_aliases([("A", "a")]).parse_normalized("p:'A'"), + parse_normalized("p:'A'") + ); + + // Multi-level substitution. + assert_eq!( + with_aliases([("A", "BC"), ("BC", "b|C"), ("C", "c")]).parse_normalized("A"), + parse_normalized("b|c") + ); + + // Infinite recursion, where the top-level error isn't of RecursiveAlias kind. + assert_eq!( + with_aliases([("A", "A")]).parse("A").unwrap_err().kind, + FilesetParseErrorKind::InAliasExpansion("A".to_owned()) + ); + assert_eq!( + with_aliases([("A", "B"), ("B", "b|C"), ("C", "c|B")]) + .parse("A") + .unwrap_err() + .kind, + FilesetParseErrorKind::InAliasExpansion("A".to_owned()) + ); + + // Error in alias definition. + assert_eq!( + with_aliases([("A", "a(")]).parse("A").unwrap_err().kind, + FilesetParseErrorKind::InAliasExpansion("A".to_owned()) + ); + } + + #[test] + fn test_expand_pattern_alias() { + assert_eq!( + with_aliases([("P:x", "x")]).parse_normalized("P:a"), + parse_normalized("a") + ); + + // Argument should be resolved in the current scope. + assert_eq!( + with_aliases([("P:x", "x|a")]).parse_normalized("P:x"), + parse_normalized("x|a") + ); + // P:a -> (Q:a)&y -> (x|a)&y + assert_eq!( + with_aliases([("P:x", "(Q:x)&y"), ("Q:y", "x|y")]).parse_normalized("P:a"), + parse_normalized("(x|a)&y") + ); + + // Pattern parameter should precede the symbol alias. + assert_eq!( + with_aliases([("P:X", "X"), ("X", "x")]).parse_normalized("(P:a)|X"), + parse_normalized("a|x") + ); + + // Pattern parameter shouldn't be expanded in symbol alias. + assert_eq!( + with_aliases([("P:x", "x|A"), ("A", "x")]).parse_normalized("P:a"), + parse_normalized("a|x") + ); + + // String literal should not be substituted with pattern parameter. + assert_eq!( + with_aliases([("P:x", "x|'x'")]).parse_normalized("P:a"), + parse_normalized("a|'x'") + ); + + // Pattern and symbol aliases reside in separate namespaces. + assert_eq!( + with_aliases([("A:x", "A"), ("A", "a")]).parse_normalized("A:x"), + parse_normalized("a") + ); + + // Infinite recursion, where the top-level error isn't of RecursiveAlias kind. + assert_eq!( + with_aliases([("P:x", "Q:x"), ("Q:x", "R:x"), ("R:x", "P:x")]) + .parse("P:a") + .unwrap_err() + .kind, + FilesetParseErrorKind::InAliasExpansion("P:x".to_owned()) + ); + } + + #[test] + fn test_expand_function_alias() { + assert_eq!( + with_aliases([("F( )", "a")]).parse_normalized("F()"), + parse_normalized("a") + ); + assert_eq!( + with_aliases([("F( x )", "x")]).parse_normalized("F(a)"), + parse_normalized("a") + ); + assert_eq!( + with_aliases([("F( x, y )", "x|y")]).parse_normalized("F(a, b)"), + parse_normalized("a|b") + ); + + // Not recursion because functions are overloaded by arity. + assert_eq!( + with_aliases([("F(x)", "F(x,b)"), ("F(x,y)", "x|y")]).parse_normalized("F(a)"), + parse_normalized("a|b") + ); + + // Arguments should be resolved in the current scope. + assert_eq!( + with_aliases([("F(x,y)", "x|y")]).parse_normalized("F(a~y,b~x)"), + parse_normalized("(a~y)|(b~x)") + ); + // F(a) -> G(a)&y -> (x|a)&y + assert_eq!( + with_aliases([("F(x)", "G(x)&y"), ("G(y)", "x|y")]).parse_normalized("F(a)"), + parse_normalized("(x|a)&y") + ); + // F(G(a)) -> F(x|a) -> G(x|a)&y -> (x|(x|a))&y + assert_eq!( + with_aliases([("F(x)", "G(x)&y"), ("G(y)", "x|y")]).parse_normalized("F(G(a))"), + parse_normalized("(x|(x|a))&y") + ); + + // Function parameter should precede the symbol alias. + assert_eq!( + with_aliases([("F(X)", "X"), ("X", "x")]).parse_normalized("F(a)|X"), + parse_normalized("a|x") + ); + + // Function parameter shouldn't be expanded in symbol alias. + assert_eq!( + with_aliases([("F(x)", "x|A"), ("A", "x")]).parse_normalized("F(a)"), + parse_normalized("a|x") + ); + + // String literal should not be substituted with function parameter. + assert_eq!( + with_aliases([("F(x)", "x|'x'")]).parse_normalized("F(a)"), + parse_normalized("a|'x'") + ); + + // Function and symbol aliases reside in separate namespaces. + assert_eq!( + with_aliases([("A()", "A"), ("A", "a")]).parse_normalized("A()"), + parse_normalized("a") + ); + + // Invalid number of arguments. + assert_eq!( + with_aliases([("F()", "x")]).parse("F(a)").unwrap_err().kind, + FilesetParseErrorKind::InvalidArguments { + name: "F".to_owned(), + message: "Expected 0 arguments".to_owned() + } + ); + assert_eq!( + with_aliases([("F(x)", "x")]).parse("F()").unwrap_err().kind, + FilesetParseErrorKind::InvalidArguments { + name: "F".to_owned(), + message: "Expected 1 arguments".to_owned() + } + ); + assert_eq!( + with_aliases([("F(x,y)", "x|y")]) + .parse("F(a,b,c)") + .unwrap_err() + .kind, + FilesetParseErrorKind::InvalidArguments { + name: "F".to_owned(), + message: "Expected 2 arguments".to_owned() + } + ); + assert_eq!( + with_aliases([("F(x)", "x"), ("F(x,y)", "x|y")]) + .parse("F()") + .unwrap_err() + .kind, + FilesetParseErrorKind::InvalidArguments { + name: "F".to_owned(), + message: "Expected 1 to 2 arguments".to_owned() + } + ); + assert_eq!( + with_aliases([("F()", "x"), ("F(x,y)", "x|y")]) + .parse("F(a)") + .unwrap_err() + .kind, + FilesetParseErrorKind::InvalidArguments { + name: "F".to_owned(), + message: "Expected 0, 2 arguments".to_owned() + } + ); + + // Infinite recursion, where the top-level error isn't of RecursiveAlias kind. + assert_eq!( + with_aliases([("F(x)", "G(x)"), ("G(x)", "H(x)"), ("H(x)", "F(x)")]) + .parse("F(a)") + .unwrap_err() + .kind, + FilesetParseErrorKind::InAliasExpansion("F(x)".to_owned()) + ); + assert_eq!( + with_aliases([("F(x)", "F(x,b)"), ("F(x,y)", "F(x|y)")]) + .parse("F(a)") + .unwrap_err() + .kind, + FilesetParseErrorKind::InAliasExpansion("F(x)".to_owned()) + ); + } +} diff --git a/lib/src/hex_util.rs b/lib/core/src/hex_util.rs similarity index 100% rename from lib/src/hex_util.rs rename to lib/core/src/hex_util.rs diff --git a/lib/core/src/lib.rs b/lib/core/src/lib.rs new file mode 100644 index 00000000000..8a242ce4a28 --- /dev/null +++ b/lib/core/src/lib.rs @@ -0,0 +1,61 @@ +// Copyright 2026 The Jujutsu Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! The core library powering the Jujutsu Version Control System. It contains +//! all "base" types such as `Commit` and the `Backend` trait. + +#![warn(missing_docs)] +#![forbid(unsafe_code)] +#![deny(unused_must_use)] + +// Needed so that proc macros can be used inside jj_lib and by external crates +// that depend on it. +// See: +// - https://github.com/rust-lang/rust/issues/54647#issuecomment-432015102 +// - https://github.com/rust-lang/rust/issues/54363 +extern crate self as jj_core; + +#[macro_use] +pub mod content_hash; + +pub mod backend; +pub mod dsl_util; +pub mod file_util; +pub mod fileset_parser; +pub mod hex_util; +pub mod matchers; +pub mod object_id; +pub mod ref_name; +pub mod repo_path; +pub mod revset; +pub mod revset_parser; +pub mod signing; +pub mod workspace_store; + +#[cfg(test)] +mod tests { + use tempfile::TempDir; + + // Copied from `testutils::TestResult` to remove dependency cycle. + pub type TestResult = eyre::Result; + + /// Unlike `testutils::new_temp_dir()`, this function doesn't set up + /// hermetic Git environment. + pub fn new_temp_dir() -> TempDir { + tempfile::Builder::new() + .prefix("jj-test-") + .tempdir() + .unwrap() + } +} diff --git a/lib/core/src/matchers.rs b/lib/core/src/matchers.rs new file mode 100644 index 00000000000..62c522b68fc --- /dev/null +++ b/lib/core/src/matchers.rs @@ -0,0 +1,145 @@ +// Copyright 2026 The Jujutsu Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Contains the [`Matcher`] trait which is used for Filesystem traversal. + +use std::collections::HashSet; +use std::fmt::Debug; + +use crate::repo_path::RepoPath; +use crate::repo_path::RepoPathComponentBuf; + +/// Describes how to traverse a Filesystem or Tree. +#[derive(PartialEq, Eq, Debug)] +pub enum Visit { + /// Everything in the directory is *guaranteed* to match, no need to check + /// descendants + AllRecursively, + /// Visit only the specified directories or files. + Specific { + /// Visit these specific directories. + dirs: VisitDirs, + /// Visit these specific files. + files: VisitFiles, + }, + /// Nothing in the directory or its subdirectories will match. + /// + /// This is the same as `Specific` with no directories or files. Use + /// `Visit::set()` to get create an instance that's `Specific` or + /// `Nothing` depending on the values at runtime. + Nothing, +} + +impl Visit { + /// All entries in the directory need to be visited, but they are not + /// guaranteed to match. + pub const SOME: Self = Self::Specific { + dirs: VisitDirs::All, + files: VisitFiles::All, + }; + + /// Visit these sets of `dirs` and `files`. + pub fn sets(dirs: HashSet, files: HashSet) -> Self { + if dirs.is_empty() && files.is_empty() { + Self::Nothing + } else { + Self::Specific { + dirs: VisitDirs::Set(dirs), + files: VisitFiles::Set(files), + } + } + } + + /// Returns true if nothing is matched. + pub fn is_nothing(&self) -> bool { + *self == Self::Nothing + } +} + +/// Visit all or some specific directories. +#[derive(PartialEq, Eq, Debug)] +pub enum VisitDirs { + /// Visit all possible directories. + All, + /// Visit the specified set of directories. + Set(HashSet), +} + +/// Visit all or some specific files. +#[derive(PartialEq, Eq, Debug)] +pub enum VisitFiles { + /// Visit all possible files. + All, + /// Visit the specified set of files. + Set(HashSet), +} + +/// `Matcher`'s are used to specify how the snapshotting path traverses +/// directories and files. +pub trait Matcher: Debug + Send + Sync { + /// Returns true if the `file` matches the traversal. + fn matches(&self, file: &RepoPath) -> bool; + /// Returns a `Visit` which specifies how further traversal should commence. + fn visit(&self, dir: &RepoPath) -> Visit; +} + +impl Matcher for &T { + fn matches(&self, file: &RepoPath) -> bool { + ::matches(self, file) + } + + fn visit(&self, dir: &RepoPath) -> Visit { + ::visit(self, dir) + } +} + +impl Matcher for Box { + fn matches(&self, file: &RepoPath) -> bool { + ::matches(self, file) + } + + fn visit(&self, dir: &RepoPath) -> Visit { + ::visit(self, dir) + } +} + +/// Match no Path and don't recursively visit any subtree. +// This is a layering violation, since jj-core should just contain traits. +#[derive(PartialEq, Eq, Debug)] +pub struct NothingMatcher; + +impl Matcher for NothingMatcher { + fn matches(&self, _file: &RepoPath) -> bool { + false + } + + fn visit(&self, _dir: &RepoPath) -> Visit { + Visit::Nothing + } +} + +/// Match every Path and recursively visit any subtree. +// This is a layering violation, since jj-core should just contain traits. +#[derive(PartialEq, Eq, Debug)] +pub struct EverythingMatcher; + +impl Matcher for EverythingMatcher { + fn matches(&self, _file: &RepoPath) -> bool { + true + } + + fn visit(&self, _dir: &RepoPath) -> Visit { + Visit::AllRecursively + } +} diff --git a/lib/core/src/object_id.rs b/lib/core/src/object_id.rs new file mode 100644 index 00000000000..0a7da2585fa --- /dev/null +++ b/lib/core/src/object_id.rs @@ -0,0 +1,358 @@ +// Copyright 2026 The Jujutsu Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Utilities for binary Objects, such as a macro for a [`ObjectId`] newtype. +//! See [`id_type!`]. + +use std::fmt; +use std::fmt::Debug; + +use crate::hex_util; + +/// Describes the common functionality of a binary ID type. +pub trait ObjectId { + /// Get actual Name of underlying type. + // this strips the trailing `Id`. + fn object_type(&self) -> String; + + /// Get access to the underlying bytes. + fn as_bytes(&self) -> &[u8]; + + /// Get access to another copy of the underlying bytes. + fn to_bytes(&self) -> Vec; + + /// Convert the underlying bytes into a hex-formatted string. + fn hex(&self) -> String; +} + +/// Defines a new struct type with visibility `vis` and name `ident` containing +/// a single `Vec` used to store an identifier (typically the output of a +/// hash function) as bytes. Types defined using this macro automatically +/// implement the `ObjectId` and `ContentHash` traits. +/// Documentation comments written inside the macro definition will be captured +/// and associated with the type defined by the macro. +/// +/// Example: +/// ```no_run +/// # use jj_core::id_type; +/// use jj_core::object_id::ObjectId; +/// id_type!( +/// /// My favorite id type. +/// pub MyId { hex() } +/// ); +/// ``` +#[macro_export] +macro_rules! id_type { + ( $(#[$attr:meta])* + $vis:vis $name:ident { $hex_method:ident() } + ) => { + $(#[$attr])* + #[derive($crate::content_hash::ContentHash, PartialEq, Eq, PartialOrd, Ord, Clone, Hash)] + $vis struct $name(Vec); + $crate::object_id::impl_id_type!($name, $hex_method); + }; +} + +/// Defines the required methods needed for a [`id_type`] which is a newtype +/// over a `Vec`. +#[macro_export] +macro_rules! impl_id_type { + ($name:ident, $hex_method:ident) => { + #[allow(dead_code)] + impl $name { + /// Creates a new instance of this id type from the given bytes. + pub fn new(value: Vec) -> Self { + Self(value) + } + + /// Creates a new instance of this id type from the given byte slice. + pub fn from_bytes(bytes: &[u8]) -> Self { + Self(bytes.to_vec()) + } + + /// Parses the given hex string into an ObjectId. + /// + /// The given string must be valid. A static str is required to + /// prevent API misuse. + pub fn from_hex(hex: &'static str) -> Self { + Self::try_from_hex(hex).unwrap() + } + + /// Parses the given hex string into an ObjectId. + pub fn try_from_hex(hex: impl AsRef<[u8]>) -> Option { + $crate::hex_util::decode_hex(hex).map(Self) + } + } + + impl std::fmt::Debug for $name { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { + // TODO: should we use $hex_method here? + f.debug_tuple(stringify!($name)).field(&self.hex()).finish() + } + } + + impl std::fmt::Display for $name { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { + f.pad(&self.$hex_method()) + } + } + + impl serde::Serialize for $name { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + if serializer.is_human_readable() { + self.$hex_method().serialize(serializer) + } else { + self.as_bytes().serialize(serializer) + } + } + } + + impl $crate::object_id::ObjectId for $name { + fn object_type(&self) -> String { + stringify!($name) + .strip_suffix("Id") + .unwrap() + .to_ascii_lowercase() + .to_string() + } + + fn as_bytes(&self) -> &[u8] { + &self.0 + } + + fn to_bytes(&self) -> Vec { + self.0.clone() + } + + fn hex(&self) -> String { + $crate::hex_util::encode_hex(&self.0) + } + } + }; +} + +pub use id_type; +pub use impl_id_type; + +/// An identifier prefix (typically from a type implementing the [`ObjectId`] +/// trait) with facilities for converting between bytes and a hex string. +#[derive(Clone, PartialEq, Eq)] +pub struct HexPrefix { + // For odd-length prefixes, the lower 4 bits of the last byte are + // zero-filled (e.g. the prefix "abc" is stored in two bytes as "abc0"). + min_prefix_bytes: Vec, + has_odd_byte: bool, +} + +impl HexPrefix { + /// Returns a new `HexPrefix` or `None` if `prefix` cannot be decoded from + /// hex to bytes. + pub fn try_from_hex(prefix: impl AsRef<[u8]>) -> Option { + let (min_prefix_bytes, has_odd_byte) = hex_util::decode_hex_prefix(prefix)?; + Some(Self { + min_prefix_bytes, + has_odd_byte, + }) + } + + /// Returns a new `HexPrefix` or `None` if `prefix` cannot be decoded from + /// "reverse" hex to bytes. + pub fn try_from_reverse_hex(prefix: impl AsRef<[u8]>) -> Option { + let (min_prefix_bytes, has_odd_byte) = hex_util::decode_reverse_hex_prefix(prefix)?; + Some(Self { + min_prefix_bytes, + has_odd_byte, + }) + } + + /// Create a new `HexPrefix` from the given bytes. + pub fn from_bytes(bytes: &[u8]) -> Self { + Self { + min_prefix_bytes: bytes.to_owned(), + has_odd_byte: false, + } + } + + /// Returns a new `HexPrefix` representing the given `id`. + pub fn from_id(id: &T) -> Self { + Self::from_bytes(id.as_bytes()) + } + + /// Returns string representation of this prefix using hex digits. + pub fn hex(&self) -> String { + let mut hex_string = hex_util::encode_hex(&self.min_prefix_bytes); + if self.has_odd_byte { + hex_string.pop().unwrap(); + } + hex_string + } + + /// Returns string representation of this prefix using `z-k` "digits". + pub fn reverse_hex(&self) -> String { + let mut hex_string = hex_util::encode_reverse_hex(&self.min_prefix_bytes); + if self.has_odd_byte { + hex_string.pop().unwrap(); + } + hex_string + } + + /// Minimum bytes that would match this prefix. (e.g. "abc0" for "abc") + /// + /// Use this to partition a sorted slice, and test `matches(id)` from there. + pub fn min_prefix_bytes(&self) -> &[u8] { + &self.min_prefix_bytes + } + + /// Returns the bytes representation if this prefix can be a full id. + pub fn as_full_bytes(&self) -> Option<&[u8]> { + (!self.has_odd_byte).then_some(&self.min_prefix_bytes) + } + + fn split_odd_byte(&self) -> (Option, &[u8]) { + if self.has_odd_byte { + let (&odd, prefix) = self.min_prefix_bytes.split_last().unwrap(); + (Some(odd), prefix) + } else { + (None, &self.min_prefix_bytes) + } + } + + /// Returns whether the stored prefix matches the prefix of `id`. + pub fn matches(&self, id: &Q) -> bool { + let id_bytes = id.as_bytes(); + let (maybe_odd, prefix) = self.split_odd_byte(); + if id_bytes.starts_with(prefix) { + if let Some(odd) = maybe_odd { + matches!(id_bytes.get(prefix.len()), Some(v) if v & 0xf0 == odd) + } else { + true + } + } else { + false + } + } +} + +impl Debug for HexPrefix { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { + f.debug_tuple("HexPrefix").field(&self.hex()).finish() + } +} + +/// The result of a prefix search. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum PrefixResolution { + /// Nothing matched. + NoMatch, + /// There was a single match, containing `T`. + SingleMatch(T), + /// There were multiple matches and thus ambiguous. + AmbiguousMatch, +} + +impl PrefixResolution { + /// Apply `f` on this `PrefixResolution` returning the new result. + pub fn map(self, f: impl FnOnce(T) -> U) -> PrefixResolution { + match self { + Self::NoMatch => PrefixResolution::NoMatch, + Self::SingleMatch(x) => PrefixResolution::SingleMatch(f(x)), + Self::AmbiguousMatch => PrefixResolution::AmbiguousMatch, + } + } + + /// Apply `f` on this `PrefixResolution` returning the new result if there's + /// a single match. + pub fn filter_map(self, f: impl FnOnce(T) -> Option) -> PrefixResolution { + match self { + Self::NoMatch => PrefixResolution::NoMatch, + Self::SingleMatch(x) => match f(x) { + None => PrefixResolution::NoMatch, + Some(y) => PrefixResolution::SingleMatch(y), + }, + Self::AmbiguousMatch => PrefixResolution::AmbiguousMatch, + } + } +} + +impl PrefixResolution { + /// Combine the results of two different `PrefixResolution`'s. + pub fn plus(&self, other: &Self) -> Self { + match (self, other) { + (Self::NoMatch, other) => other.clone(), + (local, Self::NoMatch) => local.clone(), + (Self::AmbiguousMatch, _) => Self::AmbiguousMatch, + (_, Self::AmbiguousMatch) => Self::AmbiguousMatch, + (Self::SingleMatch(_), Self::SingleMatch(_)) => Self::AmbiguousMatch, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::backend::ChangeId; + use crate::backend::CommitId; + + #[test] + fn test_display_object_id() { + let commit_id = CommitId::from_hex("deadbeef0123"); + assert_eq!(format!("{commit_id}"), "deadbeef0123"); + assert_eq!(format!("{commit_id:.6}"), "deadbe"); + + let change_id = ChangeId::from_hex("deadbeef0123"); + assert_eq!(format!("{change_id}"), "mlpmollkzyxw"); + assert_eq!(format!("{change_id:.6}"), "mlpmol"); + } + + #[test] + fn test_hex_prefix_prefixes() { + let prefix = HexPrefix::try_from_hex("").unwrap(); + assert_eq!(prefix.min_prefix_bytes(), b""); + + let prefix = HexPrefix::try_from_hex("1").unwrap(); + assert_eq!(prefix.min_prefix_bytes(), b"\x10"); + + let prefix = HexPrefix::try_from_hex("12").unwrap(); + assert_eq!(prefix.min_prefix_bytes(), b"\x12"); + + let prefix = HexPrefix::try_from_hex("123").unwrap(); + assert_eq!(prefix.min_prefix_bytes(), b"\x12\x30"); + + let bad_prefix = HexPrefix::try_from_hex("0x123"); + assert_eq!(bad_prefix, None); + + let bad_prefix = HexPrefix::try_from_hex("foobar"); + assert_eq!(bad_prefix, None); + } + + #[test] + fn test_hex_prefix_matches() { + let id = CommitId::from_hex("1234"); + + assert!(HexPrefix::try_from_hex("").unwrap().matches(&id)); + assert!(HexPrefix::try_from_hex("1").unwrap().matches(&id)); + assert!(HexPrefix::try_from_hex("12").unwrap().matches(&id)); + assert!(HexPrefix::try_from_hex("123").unwrap().matches(&id)); + assert!(HexPrefix::try_from_hex("1234").unwrap().matches(&id)); + assert!(!HexPrefix::try_from_hex("12345").unwrap().matches(&id)); + + assert!(!HexPrefix::try_from_hex("a").unwrap().matches(&id)); + assert!(!HexPrefix::try_from_hex("1a").unwrap().matches(&id)); + assert!(!HexPrefix::try_from_hex("12a").unwrap().matches(&id)); + assert!(!HexPrefix::try_from_hex("123a").unwrap().matches(&id)); + } +} diff --git a/lib/src/ref_name.rs b/lib/core/src/ref_name.rs similarity index 99% rename from lib/src/ref_name.rs rename to lib/core/src/ref_name.rs index 1b8926e4599..7ee5816ccf8 100644 --- a/lib/src/ref_name.rs +++ b/lib/core/src/ref_name.rs @@ -16,18 +16,18 @@ //! //! Name types can be constructed from a string: //! ``` -//! # use jj_lib::ref_name::*; +//! # use jj_core::ref_name::*; //! let _: RefNameBuf = "main".into(); //! let _: &RemoteName = "origin".as_ref(); //! ``` //! //! However, they cannot be converted to other name types: //! ```compile_fail -//! # use jj_lib::ref_name::*; +//! # use jj_core::ref_name::*; //! let _: RefNameBuf = RemoteName::new("origin").into(); //! ``` //! ```compile_fail -//! # use jj_lib::ref_name::*; +//! # use jj_core::ref_name::*; //! let _: &RemoteName = RefName::new("main").as_ref(); //! ``` diff --git a/lib/core/src/repo_path.rs b/lib/core/src/repo_path.rs new file mode 100644 index 00000000000..face177b57f --- /dev/null +++ b/lib/core/src/repo_path.rs @@ -0,0 +1,1191 @@ +// Copyright 2026 The Jujutsu Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! A [`RepoPath`] is a path relative to the repo root. It uses forward slashes +//! as directory separators regardless of platform. It is always valid UTF-8. + +use std::borrow::Borrow; +use std::cmp::Ordering; +use std::fmt; +use std::fmt::Debug; +use std::fmt::Formatter; +use std::iter::FusedIterator; +use std::ops::Deref; +use std::path::Component; +use std::path::Path; +use std::path::PathBuf; + +use ref_cast::RefCastCustom; +use ref_cast::ref_cast_custom; +use thiserror::Error; + +use crate::content_hash::ContentHash; +use crate::file_util; + +/// Owned `RepoPath` component. +#[derive(ContentHash, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct RepoPathComponentBuf { + // Don't add more fields. Eq, Hash, and Ord must be compatible with the + // borrowed RepoPathComponent type. + value: String, +} + +impl RepoPathComponentBuf { + /// Wraps `value` as `RepoPathComponentBuf`. + /// + /// Returns an error if the input `value` is empty or contains path + /// separator. + pub fn new(value: impl Into) -> Result { + let value: String = value.into(); + if is_valid_repo_path_component_str(&value) { + Ok(Self { value }) + } else { + Err(InvalidNewRepoPathError { value }) + } + } +} + +/// Borrowed `RepoPath` component. +#[derive(PartialEq, Eq, PartialOrd, Ord, Hash, RefCastCustom)] +#[repr(transparent)] +pub struct RepoPathComponent { + value: str, +} + +impl RepoPathComponent { + /// Wraps `value` as `RepoPathComponent`. + /// + /// Returns an error if the input `value` is empty or contains path + /// separator. + pub fn new(value: &str) -> Result<&Self, InvalidNewRepoPathError> { + if is_valid_repo_path_component_str(value) { + Ok(Self::new_unchecked(value)) + } else { + Err(InvalidNewRepoPathError { + value: value.to_string(), + }) + } + } + + #[ref_cast_custom] + const fn new_unchecked(value: &str) -> &Self; + + /// Returns the underlying string representation. + pub fn as_internal_str(&self) -> &str { + &self.value + } + + /// Returns a normal filesystem entry name if this path component is valid + /// as a file/directory name. + pub fn to_fs_name(&self) -> Result<&str, InvalidRepoPathComponentError> { + let mut components = Path::new(&self.value).components().fuse(); + match (components.next(), components.next()) { + // Trailing "." can be normalized by Path::components(), so compare + // component name. e.g. "foo\." (on Windows) should be rejected. + (Some(Component::Normal(name)), None) if name == &self.value => Ok(&self.value), + // e.g. ".", "..", "foo\bar" (on Windows) + _ => Err(InvalidRepoPathComponentError { + component: self.value.into(), + }), + } + } +} + +impl Debug for RepoPathComponent { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "{:?}", &self.value) + } +} + +impl Debug for RepoPathComponentBuf { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + ::fmt(self, f) + } +} + +impl AsRef for RepoPathComponent { + fn as_ref(&self) -> &Self { + self + } +} + +impl AsRef for RepoPathComponentBuf { + fn as_ref(&self) -> &RepoPathComponent { + self + } +} + +impl Borrow for RepoPathComponentBuf { + fn borrow(&self) -> &RepoPathComponent { + self + } +} + +impl Deref for RepoPathComponentBuf { + type Target = RepoPathComponent; + + fn deref(&self) -> &Self::Target { + RepoPathComponent::new_unchecked(&self.value) + } +} + +impl ToOwned for RepoPathComponent { + type Owned = RepoPathComponentBuf; + + fn to_owned(&self) -> Self::Owned { + let value = self.value.to_owned(); + RepoPathComponentBuf { value } + } + + fn clone_into(&self, target: &mut Self::Owned) { + self.value.clone_into(&mut target.value); + } +} + +/// Iterator over `RepoPath` components. +#[derive(Clone, Debug)] +pub struct RepoPathComponentsIter<'a> { + value: &'a str, +} + +impl<'a> RepoPathComponentsIter<'a> { + /// Returns the remaining part as repository path. + pub fn as_path(&self) -> &'a RepoPath { + RepoPath::from_internal_string_unchecked(self.value) + } +} + +impl<'a> Iterator for RepoPathComponentsIter<'a> { + type Item = &'a RepoPathComponent; + + fn next(&mut self) -> Option { + if self.value.is_empty() { + return None; + } + let (name, remainder) = self + .value + .split_once('/') + .unwrap_or_else(|| (self.value, &self.value[self.value.len()..])); + self.value = remainder; + Some(RepoPathComponent::new_unchecked(name)) + } +} + +impl DoubleEndedIterator for RepoPathComponentsIter<'_> { + fn next_back(&mut self) -> Option { + if self.value.is_empty() { + return None; + } + let (remainder, name) = self + .value + .rsplit_once('/') + .unwrap_or_else(|| (&self.value[..0], self.value)); + self.value = remainder; + Some(RepoPathComponent::new_unchecked(name)) + } +} + +impl FusedIterator for RepoPathComponentsIter<'_> {} + +/// Owned repository path. +#[derive(ContentHash, Clone, Eq, Hash, PartialEq, serde::Serialize)] +#[serde(transparent)] +pub struct RepoPathBuf { + // Don't add more fields. Eq, Hash, and Ord must be compatible with the + // borrowed RepoPath type. + value: String, +} + +/// Borrowed repository path. +#[derive(ContentHash, Eq, Hash, PartialEq, RefCastCustom, serde::Serialize)] +#[repr(transparent)] +#[serde(transparent)] +pub struct RepoPath { + value: str, +} + +impl Debug for RepoPath { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "{:?}", &self.value) + } +} + +impl Debug for RepoPathBuf { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + ::fmt(self, f) + } +} + +/// The `value` is not a valid repo path because it contains empty path +/// component. For example, `"/"`, `"/foo"`, `"foo/"`, `"foo//bar"` are all +/// invalid. +#[derive(Clone, Debug, Eq, Error, PartialEq)] +#[error(r#"Invalid repo path input "{value}""#)] +pub struct InvalidNewRepoPathError { + value: String, +} + +impl RepoPathBuf { + /// Creates owned repository path pointing to the root. + pub const fn root() -> Self { + Self { + value: String::new(), + } + } + + /// Creates `RepoPathBuf` from valid string representation. + pub fn from_internal_string(value: impl Into) -> Result { + let value: String = value.into(); + if is_valid_repo_path_str(&value) { + Ok(Self { value }) + } else { + Err(InvalidNewRepoPathError { value }) + } + } + + /// Converts repo-relative `Path` to `RepoPathBuf`. + /// + /// The input path should not contain redundant `.` or `..`. + pub fn from_relative_path( + relative_path: impl AsRef, + ) -> Result { + let relative_path = relative_path.as_ref(); + if relative_path == Path::new(".") { + return Ok(Self::root()); + } + + let mut components = relative_path + .components() + .map(|c| match c { + Component::Normal(name) => { + name.to_str() + .ok_or_else(|| RelativePathParseError::InvalidUtf8 { + path: relative_path.into(), + }) + } + _ => Err(RelativePathParseError::InvalidComponent { + component: c.as_os_str().to_string_lossy().into(), + path: relative_path.into(), + }), + }) + .fuse(); + let mut value = String::with_capacity(relative_path.as_os_str().len()); + if let Some(name) = components.next() { + value.push_str(name?); + } + for name in components { + value.push('/'); + value.push_str(name?); + } + Ok(Self { value }) + } + + /// Parses an `input` path into a `RepoPathBuf` relative to `base`. + /// + /// The `cwd` and `base` paths are supposed to be absolute and normalized in + /// the same manner. The `input` path may be either relative to `cwd` or + /// absolute. + pub fn parse_fs_path( + cwd: &Path, + base: &Path, + input: impl AsRef, + ) -> Result { + let input = input.as_ref(); + let abs_input_path = file_util::normalize_path(&cwd.join(input)); + let repo_relative_path = file_util::relative_path(base, &abs_input_path); + Self::from_relative_path(repo_relative_path).map_err(|source| FsPathParseError { + base: file_util::relative_path(cwd, base).into(), + input: input.into(), + source, + }) + } + + /// Consumes this and returns the underlying string representation. + pub fn into_internal_string(self) -> String { + self.value + } +} + +impl RepoPath { + /// Returns repository path pointing to the root. + pub const fn root() -> &'static Self { + Self::from_internal_string_unchecked("") + } + + /// Wraps valid string representation as `RepoPath`. + /// + /// Returns an error if the input `value` contains empty path component. For + /// example, `"/"`, `"/foo"`, `"foo/"`, `"foo//bar"` are all invalid. + pub fn from_internal_string(value: &str) -> Result<&Self, InvalidNewRepoPathError> { + if is_valid_repo_path_str(value) { + Ok(Self::from_internal_string_unchecked(value)) + } else { + Err(InvalidNewRepoPathError { + value: value.to_owned(), + }) + } + } + + #[ref_cast_custom] + const fn from_internal_string_unchecked(value: &str) -> &Self; + + /// The full string form used internally, not for presenting to users (where + /// we may want to use the platform's separator). This format includes a + /// trailing slash, unless this path represents the root directory. That + /// way it can be concatenated with a basename and produce a valid path. + pub fn to_internal_dir_string(&self) -> String { + if self.value.is_empty() { + String::new() + } else { + [&self.value, "/"].concat() + } + } + + /// The full string form used internally, not for presenting to users (where + /// we may want to use the platform's separator). + pub fn as_internal_file_string(&self) -> &str { + &self.value + } + + /// Converts repository path to filesystem path relative to the `base`. + /// + /// The returned path should never contain `..`, `C:` (on Windows), etc. + /// However, it may contain reserved working-copy directories such as `.jj`. + pub fn to_fs_path(&self, base: &Path) -> Result { + let mut result = PathBuf::with_capacity(base.as_os_str().len() + self.value.len() + 1); + result.push(base); + for c in self.components() { + result.push(c.to_fs_name().map_err(|err| err.with_path(self))?); + } + if result.as_os_str().is_empty() { + result.push("."); + } + Ok(result) + } + + /// Converts repository path to filesystem path relative to the `base`, + /// without checking invalid path components. + /// + /// The returned path may point outside of the `base` directory. Use this + /// function only for displaying or testing purposes. + pub fn to_fs_path_unchecked(&self, base: &Path) -> PathBuf { + let mut result = PathBuf::with_capacity(base.as_os_str().len() + self.value.len() + 1); + result.push(base); + result.extend(self.components().map(RepoPathComponent::as_internal_str)); + if result.as_os_str().is_empty() { + result.push("."); + } + result + } + + /// Returns true if this is a root path. + pub fn is_root(&self) -> bool { + self.value.is_empty() + } + + /// Returns true if the `base` is a prefix of this path. + pub fn starts_with(&self, base: &Self) -> bool { + self.strip_prefix(base).is_some() + } + + /// Returns the remaining path with the `base` path removed. + pub fn strip_prefix(&self, base: &Self) -> Option<&Self> { + if base.value.is_empty() { + Some(self) + } else { + let tail = self.value.strip_prefix(&base.value)?; + if tail.is_empty() { + Some(Self::from_internal_string_unchecked(tail)) + } else { + tail.strip_prefix('/') + .map(Self::from_internal_string_unchecked) + } + } + } + + /// Returns the parent path without the base name component. + pub fn parent(&self) -> Option<&Self> { + self.split().map(|(parent, _)| parent) + } + + /// Splits this into the parent path and base name component. + pub fn split(&self) -> Option<(&Self, &RepoPathComponent)> { + let mut components = self.components(); + let basename = components.next_back()?; + Some((components.as_path(), basename)) + } + + /// Iterator over the path's components, with parents before children. + /// + /// For example, `RepoPath::from_internal_string("a/b/c")?.components()` + /// yields "a", "b", "c". + pub fn components(&self) -> RepoPathComponentsIter<'_> { + RepoPathComponentsIter { value: &self.value } + } + + /// Iterator over the path's ancestors, with children before parents. + /// + /// For example, `RepoPath::from_internal_string("a/b/c")?.ancestors()` + /// yiels "a/b/c", "a/b", "a", "". + pub fn ancestors(&self) -> impl Iterator { + std::iter::successors(Some(self), |path| path.parent()) + } + + /// Join the given `entry` on the Path returning a new `RepoPathBuf`. + pub fn join(&self, entry: &RepoPathComponent) -> RepoPathBuf { + let value = if self.value.is_empty() { + entry.as_internal_str().to_owned() + } else { + [&self.value, "/", entry.as_internal_str()].concat() + }; + RepoPathBuf { value } + } + + /// Splits this path at its common prefix with `other`. + /// + /// # Returns + /// + /// Returns the `(common_prefix, self_remainder)`. + /// + /// All paths will at least have `RepoPath::root()` as a common prefix, + /// therefore even if `self` and `other` have no matching parent component + /// this function will always return at least `(RepoPath::root(), self)`. + /// + /// + /// # Examples + /// + /// ``` + /// use jj_core::repo_path::RepoPath; + /// + /// let bing_path = RepoPath::from_internal_string("foo/bar/bing").unwrap(); + /// + /// let baz_path = RepoPath::from_internal_string("foo/bar/baz").unwrap(); + /// + /// let foo_bar_path = RepoPath::from_internal_string("foo/bar").unwrap(); + /// + /// assert_eq!( + /// bing_path.split_common_prefix(&baz_path), + /// (foo_bar_path, RepoPath::from_internal_string("bing").unwrap()) + /// ); + /// + /// let unrelated_path = RepoPath::from_internal_string("no/common/prefix").unwrap(); + /// assert_eq!( + /// baz_path.split_common_prefix(&unrelated_path), + /// (RepoPath::root(), baz_path) + /// ); + /// ``` + pub fn split_common_prefix(&self, other: &Self) -> (&Self, &Self) { + // Obtain the common prefix between these paths + let mut prefix_len = 0; + + let common_components = self + .components() + .zip(other.components()) + .take_while(|(prev_comp, this_comp)| prev_comp == this_comp); + + for (self_comp, _other_comp) in common_components { + if prefix_len > 0 { + // + 1 for all paths to take their separators into account. + // We skip the first one since there are ComponentCount - 1 separators in a + // path. + prefix_len += 1; + } + + prefix_len += self_comp.value.len(); + } + + if prefix_len == 0 { + // No common prefix except root + return (Self::root(), self); + } + + if prefix_len == self.value.len() { + return (self, Self::root()); + } + + let common_prefix = Self::from_internal_string_unchecked(&self.value[..prefix_len]); + let remainder = Self::from_internal_string_unchecked(&self.value[prefix_len + 1..]); + + (common_prefix, remainder) + } +} + +impl AsRef for RepoPath { + fn as_ref(&self) -> &Self { + self + } +} + +impl AsRef for RepoPathBuf { + fn as_ref(&self) -> &RepoPath { + self + } +} + +impl Borrow for RepoPathBuf { + fn borrow(&self) -> &RepoPath { + self + } +} + +impl Deref for RepoPathBuf { + type Target = RepoPath; + + fn deref(&self) -> &Self::Target { + RepoPath::from_internal_string_unchecked(&self.value) + } +} + +impl ToOwned for RepoPath { + type Owned = RepoPathBuf; + + fn to_owned(&self) -> Self::Owned { + let value = self.value.to_owned(); + RepoPathBuf { value } + } + + fn clone_into(&self, target: &mut Self::Owned) { + self.value.clone_into(&mut target.value); + } +} + +impl Ord for RepoPath { + fn cmp(&self, other: &Self) -> Ordering { + // If there were leading/trailing slash, components-based Ord would + // disagree with str-based Eq. + debug_assert!(is_valid_repo_path_str(&self.value)); + self.components().cmp(other.components()) + } +} + +impl Ord for RepoPathBuf { + fn cmp(&self, other: &Self) -> Ordering { + ::cmp(self, other) + } +} + +impl PartialOrd for RepoPath { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl PartialOrd for RepoPathBuf { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl> Extend

for RepoPathBuf { + fn extend>(&mut self, iter: T) { + for component in iter { + if !self.value.is_empty() { + self.value.push('/'); + } + self.value.push_str(component.as_ref().as_internal_str()); + } + } +} + +/// `RepoPath` contained invalid file/directory component such as `..`. +#[derive(Clone, Debug, Eq, Error, PartialEq)] +#[error(r#"Invalid repository path "{}""#, path.as_internal_file_string())] +pub struct InvalidRepoPathError { + /// Path containing an error. + pub path: RepoPathBuf, + /// Source error. + pub source: InvalidRepoPathComponentError, +} + +/// `RepoPath` component was invalid. (e.g. `..`) +#[derive(Clone, Debug, Eq, Error, PartialEq)] +#[error(r#"Invalid path component "{component}""#)] +pub struct InvalidRepoPathComponentError { + /// The invalid component. + pub component: Box, +} + +impl InvalidRepoPathComponentError { + /// Attaches the `path` that caused the error. + pub fn with_path(self, path: &RepoPath) -> InvalidRepoPathError { + InvalidRepoPathError { + path: path.to_owned(), + source: self, + } + } +} + +/// An error which occurs during relative path parsing. +#[derive(Clone, Debug, Eq, Error, PartialEq)] +pub enum RelativePathParseError { + /// An invalid component was seen. + #[error(r#"Invalid component "{component}" in repo-relative path "{path}""#)] + InvalidComponent { + /// The invalid component. + component: Box, + /// The path it was a component of. + path: Box, + }, + /// The path was not UTF-8. + #[error(r#"Not valid UTF-8 path "{path}""#)] + InvalidUtf8 { + /// The path which did not contain UTF-8 characters. + path: Box, + }, +} + +/// An error which occurs when we're parsing paths. +#[derive(Clone, Debug, Eq, Error, PartialEq)] +#[error(r#"Path "{input}" is not in the repo "{base}""#)] +pub struct FsPathParseError { + /// Repository or workspace root path relative to the `cwd`. + pub base: Box, + /// Input path without normalization. + pub input: Box, + /// Source error. + pub source: RelativePathParseError, +} + +fn is_valid_repo_path_component_str(value: &str) -> bool { + !value.is_empty() && !value.contains('/') +} + +fn is_valid_repo_path_str(value: &str) -> bool { + !value.starts_with('/') && !value.ends_with('/') && !value.contains("//") +} + +#[cfg(test)] +mod tests { + use std::panic; + + use assert_matches::assert_matches; + use itertools::Itertools as _; + + use super::*; + use crate::tests::new_temp_dir; + + fn repo_path(value: &str) -> &RepoPath { + RepoPath::from_internal_string(value).unwrap() + } + + fn repo_path_component(value: &str) -> &RepoPathComponent { + RepoPathComponent::new(value).unwrap() + } + + #[test] + fn test_is_root() { + assert!(RepoPath::root().is_root()); + assert!(repo_path("").is_root()); + assert!(!repo_path("foo").is_root()); + } + + #[test] + fn test_from_internal_string() { + let repo_path_buf = |value: &str| RepoPathBuf::from_internal_string(value).unwrap(); + assert_eq!(repo_path_buf(""), RepoPathBuf::root()); + assert!(panic::catch_unwind(|| repo_path_buf("/")).is_err()); + assert!(panic::catch_unwind(|| repo_path_buf("/x")).is_err()); + assert!(panic::catch_unwind(|| repo_path_buf("x/")).is_err()); + assert!(panic::catch_unwind(|| repo_path_buf("x//y")).is_err()); + + assert_eq!(repo_path(""), RepoPath::root()); + assert!(panic::catch_unwind(|| repo_path("/")).is_err()); + assert!(panic::catch_unwind(|| repo_path("/x")).is_err()); + assert!(panic::catch_unwind(|| repo_path("x/")).is_err()); + assert!(panic::catch_unwind(|| repo_path("x//y")).is_err()); + } + + #[test] + fn test_as_internal_file_string() { + assert_eq!(RepoPath::root().as_internal_file_string(), ""); + assert_eq!(repo_path("dir").as_internal_file_string(), "dir"); + assert_eq!(repo_path("dir/file").as_internal_file_string(), "dir/file"); + } + + #[test] + fn test_to_internal_dir_string() { + assert_eq!(RepoPath::root().to_internal_dir_string(), ""); + assert_eq!(repo_path("dir").to_internal_dir_string(), "dir/"); + assert_eq!(repo_path("dir/file").to_internal_dir_string(), "dir/file/"); + } + + #[test] + fn test_starts_with() { + assert!(repo_path("").starts_with(repo_path(""))); + assert!(repo_path("x").starts_with(repo_path(""))); + assert!(!repo_path("").starts_with(repo_path("x"))); + + assert!(repo_path("x").starts_with(repo_path("x"))); + assert!(repo_path("x/y").starts_with(repo_path("x"))); + assert!(!repo_path("xy").starts_with(repo_path("x"))); + assert!(!repo_path("x/y").starts_with(repo_path("y"))); + + assert!(repo_path("x/y").starts_with(repo_path("x/y"))); + assert!(repo_path("x/y/z").starts_with(repo_path("x/y"))); + assert!(!repo_path("x/yz").starts_with(repo_path("x/y"))); + assert!(!repo_path("x").starts_with(repo_path("x/y"))); + assert!(!repo_path("xy").starts_with(repo_path("x/y"))); + } + + #[test] + fn test_strip_prefix() { + assert_eq!( + repo_path("").strip_prefix(repo_path("")), + Some(repo_path("")) + ); + assert_eq!( + repo_path("x").strip_prefix(repo_path("")), + Some(repo_path("x")) + ); + assert_eq!(repo_path("").strip_prefix(repo_path("x")), None); + + assert_eq!( + repo_path("x").strip_prefix(repo_path("x")), + Some(repo_path("")) + ); + assert_eq!( + repo_path("x/y").strip_prefix(repo_path("x")), + Some(repo_path("y")) + ); + assert_eq!(repo_path("xy").strip_prefix(repo_path("x")), None); + assert_eq!(repo_path("x/y").strip_prefix(repo_path("y")), None); + + assert_eq!( + repo_path("x/y").strip_prefix(repo_path("x/y")), + Some(repo_path("")) + ); + assert_eq!( + repo_path("x/y/z").strip_prefix(repo_path("x/y")), + Some(repo_path("z")) + ); + assert_eq!(repo_path("x/yz").strip_prefix(repo_path("x/y")), None); + assert_eq!(repo_path("x").strip_prefix(repo_path("x/y")), None); + assert_eq!(repo_path("xy").strip_prefix(repo_path("x/y")), None); + } + + #[test] + fn test_order() { + assert!(RepoPath::root() < repo_path("dir")); + assert!(repo_path("dir") < repo_path("dirx")); + // '#' < '/', but ["dir", "sub"] < ["dir#"] + assert!(repo_path("dir") < repo_path("dir#")); + assert!(repo_path("dir") < repo_path("dir/sub")); + assert!(repo_path("dir/sub") < repo_path("dir#")); + + assert!(repo_path("abc") < repo_path("dir/file")); + assert!(repo_path("dir") < repo_path("dir/file")); + assert!(repo_path("dis") > repo_path("dir/file")); + assert!(repo_path("xyz") > repo_path("dir/file")); + assert!(repo_path("dir1/xyz") < repo_path("dir2/abc")); + } + + #[test] + fn test_join() { + let root = RepoPath::root(); + let dir = root.join(repo_path_component("dir")); + assert_eq!(dir.as_ref(), repo_path("dir")); + let subdir = dir.join(repo_path_component("subdir")); + assert_eq!(subdir.as_ref(), repo_path("dir/subdir")); + assert_eq!( + subdir.join(repo_path_component("file")).as_ref(), + repo_path("dir/subdir/file") + ); + } + + #[test] + fn test_extend() { + let mut path = RepoPathBuf::root(); + path.extend(std::iter::empty::()); + assert_eq!(path.as_ref(), RepoPath::root()); + path.extend([repo_path_component("dir")]); + assert_eq!(path.as_ref(), repo_path("dir")); + path.extend(std::iter::repeat_n(repo_path_component("subdir"), 3)); + assert_eq!(path.as_ref(), repo_path("dir/subdir/subdir/subdir")); + path.extend(std::iter::empty::()); + assert_eq!(path.as_ref(), repo_path("dir/subdir/subdir/subdir")); + } + + #[test] + fn test_parent() { + let root = RepoPath::root(); + let dir_component = repo_path_component("dir"); + let subdir_component = repo_path_component("subdir"); + + let dir = root.join(dir_component); + let subdir = dir.join(subdir_component); + + assert_eq!(root.parent(), None); + assert_eq!(dir.parent(), Some(root)); + assert_eq!(subdir.parent(), Some(dir.as_ref())); + } + + #[test] + fn test_split() { + let root = RepoPath::root(); + let dir_component = repo_path_component("dir"); + let file_component = repo_path_component("file"); + + let dir = root.join(dir_component); + let file = dir.join(file_component); + + assert_eq!(root.split(), None); + assert_eq!(dir.split(), Some((root, dir_component))); + assert_eq!(file.split(), Some((dir.as_ref(), file_component))); + } + + #[test] + fn test_components() { + assert!(RepoPath::root().components().next().is_none()); + assert_eq!( + repo_path("dir").components().collect_vec(), + vec![repo_path_component("dir")] + ); + assert_eq!( + repo_path("dir/subdir").components().collect_vec(), + vec![repo_path_component("dir"), repo_path_component("subdir")] + ); + + // Iterates from back + assert!(RepoPath::root().components().next_back().is_none()); + assert_eq!( + repo_path("dir").components().rev().collect_vec(), + vec![repo_path_component("dir")] + ); + assert_eq!( + repo_path("dir/subdir").components().rev().collect_vec(), + vec![repo_path_component("subdir"), repo_path_component("dir")] + ); + } + + #[test] + fn test_ancestors() { + assert_eq!( + RepoPath::root().ancestors().collect_vec(), + vec![RepoPath::root()] + ); + assert_eq!( + repo_path("dir").ancestors().collect_vec(), + vec![repo_path("dir"), RepoPath::root()] + ); + assert_eq!( + repo_path("dir/subdir").ancestors().collect_vec(), + vec![repo_path("dir/subdir"), repo_path("dir"), RepoPath::root()] + ); + } + + #[test] + fn test_to_fs_path() { + assert_eq!( + repo_path("").to_fs_path(Path::new("base/dir")).unwrap(), + Path::new("base/dir") + ); + assert_eq!( + repo_path("").to_fs_path(Path::new("")).unwrap(), + Path::new(".") + ); + assert_eq!( + repo_path("file").to_fs_path(Path::new("base/dir")).unwrap(), + Path::new("base/dir/file") + ); + assert_eq!( + repo_path("some/deep/dir/file") + .to_fs_path(Path::new("base/dir")) + .unwrap(), + Path::new("base/dir/some/deep/dir/file") + ); + assert_eq!( + repo_path("dir/file").to_fs_path(Path::new("")).unwrap(), + Path::new("dir/file") + ); + + // Current/parent dir component + assert!(repo_path(".").to_fs_path(Path::new("base")).is_err()); + assert!(repo_path("..").to_fs_path(Path::new("base")).is_err()); + assert!( + repo_path("dir/../file") + .to_fs_path(Path::new("base")) + .is_err() + ); + assert!(repo_path("./file").to_fs_path(Path::new("base")).is_err()); + assert!(repo_path("file/.").to_fs_path(Path::new("base")).is_err()); + assert!(repo_path("../file").to_fs_path(Path::new("base")).is_err()); + assert!(repo_path("file/..").to_fs_path(Path::new("base")).is_err()); + + // Empty component (which is invalid as a repo path) + assert!( + RepoPath::from_internal_string_unchecked("/") + .to_fs_path(Path::new("base")) + .is_err() + ); + assert_eq!( + // Iterator omits empty component after "/", which is fine so long + // as the returned path doesn't escape. + RepoPath::from_internal_string_unchecked("a/") + .to_fs_path(Path::new("base")) + .unwrap(), + Path::new("base/a") + ); + assert!( + RepoPath::from_internal_string_unchecked("/b") + .to_fs_path(Path::new("base")) + .is_err() + ); + assert!( + RepoPath::from_internal_string_unchecked("a//b") + .to_fs_path(Path::new("base")) + .is_err() + ); + + // Component containing slash (simulating Windows path separator) + assert!( + RepoPathComponent::new_unchecked("wind/ows") + .to_fs_name() + .is_err() + ); + assert!( + RepoPathComponent::new_unchecked("./file") + .to_fs_name() + .is_err() + ); + assert!( + RepoPathComponent::new_unchecked("file/.") + .to_fs_name() + .is_err() + ); + assert!(RepoPathComponent::new_unchecked("/").to_fs_name().is_err()); + + // Windows path separator and drive letter + if cfg!(windows) { + assert!( + repo_path(r#"wind\ows"#) + .to_fs_path(Path::new("base")) + .is_err() + ); + assert!( + repo_path(r#".\file"#) + .to_fs_path(Path::new("base")) + .is_err() + ); + assert!( + repo_path(r#"file\."#) + .to_fs_path(Path::new("base")) + .is_err() + ); + assert!( + repo_path(r#"c:/foo"#) + .to_fs_path(Path::new("base")) + .is_err() + ); + } + } + + #[test] + fn test_to_fs_path_unchecked() { + assert_eq!( + repo_path("").to_fs_path_unchecked(Path::new("base/dir")), + Path::new("base/dir") + ); + assert_eq!( + repo_path("").to_fs_path_unchecked(Path::new("")), + Path::new(".") + ); + assert_eq!( + repo_path("file").to_fs_path_unchecked(Path::new("base/dir")), + Path::new("base/dir/file") + ); + assert_eq!( + repo_path("some/deep/dir/file").to_fs_path_unchecked(Path::new("base/dir")), + Path::new("base/dir/some/deep/dir/file") + ); + assert_eq!( + repo_path("dir/file").to_fs_path_unchecked(Path::new("")), + Path::new("dir/file") + ); + } + + #[test] + fn parse_fs_path_wc_in_cwd() { + let temp_dir = new_temp_dir(); + let cwd_path = temp_dir.path().join("repo"); + let wc_path = &cwd_path; + + assert_eq!( + RepoPathBuf::parse_fs_path(&cwd_path, wc_path, "").as_deref(), + Ok(RepoPath::root()) + ); + assert_eq!( + RepoPathBuf::parse_fs_path(&cwd_path, wc_path, ".").as_deref(), + Ok(RepoPath::root()) + ); + assert_eq!( + RepoPathBuf::parse_fs_path(&cwd_path, wc_path, "file").as_deref(), + Ok(repo_path("file")) + ); + // Both slash and the platform's separator are allowed + assert_eq!( + RepoPathBuf::parse_fs_path( + &cwd_path, + wc_path, + format!("dir{}file", std::path::MAIN_SEPARATOR) + ) + .as_deref(), + Ok(repo_path("dir/file")) + ); + assert_eq!( + RepoPathBuf::parse_fs_path(&cwd_path, wc_path, "dir/file").as_deref(), + Ok(repo_path("dir/file")) + ); + assert_matches!( + RepoPathBuf::parse_fs_path(&cwd_path, wc_path, ".."), + Err(FsPathParseError { + source: RelativePathParseError::InvalidComponent { .. }, + .. + }) + ); + assert_eq!( + RepoPathBuf::parse_fs_path(&cwd_path, &cwd_path, "../repo").as_deref(), + Ok(RepoPath::root()) + ); + assert_eq!( + RepoPathBuf::parse_fs_path(&cwd_path, &cwd_path, "../repo/file").as_deref(), + Ok(repo_path("file")) + ); + // Input may be absolute path with ".." + assert_eq!( + RepoPathBuf::parse_fs_path( + &cwd_path, + &cwd_path, + cwd_path.join("../repo").to_str().unwrap() + ) + .as_deref(), + Ok(RepoPath::root()) + ); + } + + #[test] + fn parse_fs_path_wc_in_cwd_parent() { + let temp_dir = new_temp_dir(); + let cwd_path = temp_dir.path().join("dir"); + let wc_path = cwd_path.parent().unwrap().to_path_buf(); + + assert_eq!( + RepoPathBuf::parse_fs_path(&cwd_path, &wc_path, "").as_deref(), + Ok(repo_path("dir")) + ); + assert_eq!( + RepoPathBuf::parse_fs_path(&cwd_path, &wc_path, ".").as_deref(), + Ok(repo_path("dir")) + ); + assert_eq!( + RepoPathBuf::parse_fs_path(&cwd_path, &wc_path, "file").as_deref(), + Ok(repo_path("dir/file")) + ); + assert_eq!( + RepoPathBuf::parse_fs_path(&cwd_path, &wc_path, "subdir/file").as_deref(), + Ok(repo_path("dir/subdir/file")) + ); + assert_eq!( + RepoPathBuf::parse_fs_path(&cwd_path, &wc_path, "..").as_deref(), + Ok(RepoPath::root()) + ); + assert_matches!( + RepoPathBuf::parse_fs_path(&cwd_path, &wc_path, "../.."), + Err(FsPathParseError { + source: RelativePathParseError::InvalidComponent { .. }, + .. + }) + ); + assert_eq!( + RepoPathBuf::parse_fs_path(&cwd_path, &wc_path, "../other-dir/file").as_deref(), + Ok(repo_path("other-dir/file")) + ); + } + + #[test] + fn parse_fs_path_wc_in_cwd_child() { + let temp_dir = new_temp_dir(); + let cwd_path = temp_dir.path().join("cwd"); + let wc_path = cwd_path.join("repo"); + + assert_matches!( + RepoPathBuf::parse_fs_path(&cwd_path, &wc_path, ""), + Err(FsPathParseError { + source: RelativePathParseError::InvalidComponent { .. }, + .. + }) + ); + assert_matches!( + RepoPathBuf::parse_fs_path(&cwd_path, &wc_path, "not-repo"), + Err(FsPathParseError { + source: RelativePathParseError::InvalidComponent { .. }, + .. + }) + ); + assert_eq!( + RepoPathBuf::parse_fs_path(&cwd_path, &wc_path, "repo").as_deref(), + Ok(RepoPath::root()) + ); + assert_eq!( + RepoPathBuf::parse_fs_path(&cwd_path, &wc_path, "repo/file").as_deref(), + Ok(repo_path("file")) + ); + assert_eq!( + RepoPathBuf::parse_fs_path(&cwd_path, &wc_path, "repo/dir/file").as_deref(), + Ok(repo_path("dir/file")) + ); + } + + #[test] + fn test_split_common_prefix() { + assert_eq!( + repo_path("foo/bar").split_common_prefix(repo_path("foo/bar/baz")), + (repo_path("foo/bar"), repo_path("")) + ); + + assert_eq!( + repo_path("foo/bar/baz").split_common_prefix(repo_path("foo/bar")), + (repo_path("foo/bar"), repo_path("baz")) + ); + + assert_eq!( + repo_path("foo/bar/bing").split_common_prefix(repo_path("foo/bar/baz")), + (repo_path("foo/bar"), repo_path("bing")) + ); + + assert_eq!( + repo_path("no/common/prefix").split_common_prefix(repo_path("foo/bar/baz")), + (RepoPath::root(), repo_path("no/common/prefix")) + ); + + assert_eq!( + repo_path("same/path").split_common_prefix(repo_path("same/path")), + (repo_path("same/path"), RepoPath::root()) + ); + + assert_eq!( + RepoPath::root().split_common_prefix(repo_path("foo")), + (RepoPath::root(), RepoPath::root()) + ); + + assert_eq!( + RepoPath::root().split_common_prefix(RepoPath::root()), + (RepoPath::root(), RepoPath::root()) + ); + + assert_eq!( + repo_path("foo/bar").split_common_prefix(RepoPath::root()), + (RepoPath::root(), repo_path("foo/bar")) + ); + } +} diff --git a/lib/src/revset.pest b/lib/core/src/revset.pest similarity index 100% rename from lib/src/revset.pest rename to lib/core/src/revset.pest diff --git a/lib/core/src/revset.rs b/lib/core/src/revset.rs new file mode 100644 index 00000000000..8b63f1e0ee5 --- /dev/null +++ b/lib/core/src/revset.rs @@ -0,0 +1,44 @@ +// Copyright 2026 The Jujutsu Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Contains some basic methods around Revsets. +// TODO: consider moving the optimizer into this. + +use crate::dsl_util; +use crate::revset_parser; + +/// Formats a string as symbol by quoting and escaping it if necessary. +/// +/// Note that symbols may be substituted to user aliases. Use +/// [`format_string()`] to ensure that the provided string is resolved as a +/// tag/bookmark name, commit/change ID prefix, etc. +pub fn format_symbol(literal: &str) -> String { + if revset_parser::is_identifier(literal) { + literal.to_string() + } else { + format_string(literal) + } +} + +/// Formats a string by quoting and escaping it. +pub fn format_string(literal: &str) -> String { + format!(r#""{}""#, dsl_util::escape_string(literal)) +} + +/// Formats a `name@remote` symbol, applies quoting and escaping if necessary. +pub fn format_remote_symbol(name: &str, remote: &str) -> String { + let name = format_symbol(name); + let remote = format_symbol(remote); + format!("{name}@{remote}") +} diff --git a/lib/core/src/revset_parser.rs b/lib/core/src/revset_parser.rs new file mode 100644 index 00000000000..530dbd24f4f --- /dev/null +++ b/lib/core/src/revset_parser.rs @@ -0,0 +1,2012 @@ +// Copyright 2021-2024 The Jujutsu Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Contains the `RevsetParser` and Revset-specific DSL helpers for alias +//! expansions. + +// TODO: finish documenting `revset.pest`. +#![allow(missing_docs)] + +use std::collections::HashSet; +use std::error; +use std::mem; +use std::str::FromStr; +use std::sync::LazyLock; + +use itertools::Itertools as _; +use pest::Parser as _; +use pest::iterators::Pair; +use pest::pratt_parser::Assoc; +use pest::pratt_parser::Op; +use pest::pratt_parser::PrattParser; +use pest_derive::Parser; +use thiserror::Error; + +use crate::dsl_util; +use crate::dsl_util::AliasDeclaration; +use crate::dsl_util::AliasDeclarationParser; +use crate::dsl_util::AliasDefinitionParser; +use crate::dsl_util::AliasExpandError; +use crate::dsl_util::AliasExpandableExpression; +use crate::dsl_util::AliasId; +use crate::dsl_util::AliasesMap; +use crate::dsl_util::Diagnostics; +use crate::dsl_util::ExpressionFolder; +use crate::dsl_util::FoldableExpression; +use crate::dsl_util::FunctionCallParser; +use crate::dsl_util::InvalidArguments; +use crate::dsl_util::StringLiteralParser; +use crate::dsl_util::collect_similar; +use crate::ref_name::RefNameBuf; +use crate::ref_name::RemoteNameBuf; +use crate::ref_name::RemoteRefSymbolBuf; + +/// The [`RevsetParser`] which is generated from the revset.pest grammar file. +#[derive(Parser)] +#[grammar = "revset.pest"] +struct RevsetParser; + +const STRING_LITERAL_PARSER: StringLiteralParser = StringLiteralParser { + content_rule: Rule::string_content, + escape_rule: Rule::string_escape, +}; +const FUNCTION_CALL_PARSER: FunctionCallParser = FunctionCallParser { + function_name_rule: Rule::function_name, + function_arguments_rule: Rule::function_arguments, + keyword_argument_rule: Rule::keyword_argument, + argument_name_rule: Rule::strict_identifier, + argument_value_rule: Rule::expression, +}; + +impl Rule { + /// Whether this is a placeholder rule for compatibility with the other + /// systems. + fn is_compat(&self) -> bool { + matches!( + self, + Self::compat_parents_op + | Self::compat_dag_range_op + | Self::compat_dag_range_pre_op + | Self::compat_dag_range_post_op + | Self::compat_add_op + | Self::compat_sub_op + ) + } + + fn to_symbol(self) -> Option<&'static str> { + match self { + Self::EOI => None, + Self::whitespace => None, + Self::identifier_part => None, + Self::identifier => None, + Self::strict_identifier_part => None, + Self::strict_identifier => None, + Self::symbol => None, + Self::string_escape => None, + Self::string_content_char => None, + Self::string_content => None, + Self::string_literal => None, + Self::raw_string_content => None, + Self::raw_string_literal => None, + Self::at_op => Some("@"), + Self::pattern_kind_op => Some(":"), + Self::parents_op => Some("-"), + Self::children_op => Some("+"), + Self::compat_parents_op => Some("^"), + Self::dag_range_op + | Self::dag_range_pre_op + | Self::dag_range_post_op + | Self::dag_range_all_op => Some("::"), + Self::compat_dag_range_op + | Self::compat_dag_range_pre_op + | Self::compat_dag_range_post_op => Some(":"), + Self::range_op => Some(".."), + Self::range_pre_op | Self::range_post_op | Self::range_all_op => Some(".."), + Self::range_ops => None, + Self::range_pre_ops => None, + Self::range_post_ops => None, + Self::range_all_ops => None, + Self::negate_op => Some("~"), + Self::union_op => Some("|"), + Self::intersection_op => Some("&"), + Self::difference_op => Some("~"), + Self::compat_add_op => Some("+"), + Self::compat_sub_op => Some("-"), + Self::infix_op => None, + Self::function => None, + Self::function_name => None, + Self::keyword_argument => None, + Self::argument => None, + Self::function_arguments => None, + Self::formal_parameters => None, + Self::pattern => None, + Self::pattern_value_expression => None, + Self::primary => None, + Self::neighbors_expression => None, + Self::range_expression => None, + Self::expression => None, + Self::program => None, + Self::symbol_name => None, + Self::function_alias_declaration => None, + Self::pattern_alias_declaration => None, + Self::alias_declaration => None, + } + } +} + +/// Manages diagnostic messages emitted during revset parsing and function-call +/// resolution. +pub type RevsetDiagnostics = Diagnostics; + +/// An error which can occur during Revset parsing. +#[derive(Debug, Error)] +#[error("{pest_error}")] +pub struct RevsetParseError { + kind: Box, + pest_error: Box>, + source: Option>, +} + +/// The RevsetError kind, so the source the error stems from. +#[derive(Debug, Error, PartialEq, Eq)] +pub enum RevsetParseErrorKind { + /// A plain Syntax error. + #[error("Syntax error")] + SyntaxError, + /// A wrongly applied operator which isn't a PrefixOperator. + #[error("`{op}` is not a prefix operator")] + NotPrefixOperator { + /// The operator. + op: String, + /// The nearest operator which matches for this case. + similar_op: String, + /// Any additional description. + description: String, + }, + /// A wrongly applied operator which isn't a PostfixOperator. + #[error("`{op}` is not a postfix operator")] + NotPostfixOperator { + /// The operator. + op: String, + /// The nearest operator which matches for this case. + similar_op: String, + /// Any additional description. + description: String, + }, + /// A wrongly applied which isn't a InfixOperator. + #[error("`{op}` is not an infix operator")] + NotInfixOperator { + /// The operator. + op: String, + /// The nearest operator which matches for this case. + similar_op: String, + /// Any additional description. + description: String, + }, + /// A function was not found or doesn't exist. + #[error("Function `{name}` doesn't exist")] + NoSuchFunction { + /// The name of the given function. + name: String, + /// A list of function candidates which match for a substring of `name`. + candidates: Vec, + }, + /// A function received wrong arguments. + #[error("Function `{name}`: {message}")] + InvalidFunctionArguments { + /// The name of the function. + name: String, + /// An additional message which is appended to the error. + message: String, + }, + /// A file pattern was passed without a workspace. + #[error("Cannot resolve file pattern without workspace")] + FsPathWithoutWorkspace, + /// The working-copy was requested without a workspace. + #[error("Cannot resolve `@` without workspace")] + WorkingCopyWithoutWorkspace, + /// A function parameter was redefined. + #[error("Redefinition of function parameter")] + RedefinedFunctionParameter, + /// An expression caused the error. + #[error("{0}")] + Expression(String), + /// A alias failed to expand. + #[error("In alias `{0}`")] + InAliasExpansion(String), + /// A parameter failed to expand. + #[error("In function parameter `{0}`")] + InParameterExpansion(String), + /// There was recursion during the alias expansion. + #[error("Alias `{0}` expanded recursively")] + RecursiveAlias(String), +} + +impl RevsetParseError { + /// Create a new `RevsetParseError` with the given `kind` and `span`. + pub fn with_span(kind: RevsetParseErrorKind, span: pest::Span<'_>) -> Self { + let message = kind.to_string(); + let pest_error = Box::new(pest::error::Error::new_from_span( + pest::error::ErrorVariant::CustomError { message }, + span, + )); + Self { + kind: Box::new(kind), + pest_error, + source: None, + } + } + + /// Add an additional error source to the `RevsetParseError`. + pub fn with_source(mut self, source: impl Into>) -> Self { + self.source = Some(source.into()); + self + } + + /// Some other expression error. + pub fn expression(message: impl Into, span: pest::Span<'_>) -> Self { + Self::with_span(RevsetParseErrorKind::Expression(message.into()), span) + } + + /// If this is a `NoSuchFunction` error, expands the candidates list with + /// the given `other_functions`. + pub fn extend_function_candidates(mut self, other_functions: I) -> Self + where + I: IntoIterator, + I::Item: AsRef, + { + if let RevsetParseErrorKind::NoSuchFunction { name, candidates } = self.kind.as_mut() { + let other_candidates = collect_similar(name, other_functions); + *candidates = itertools::merge(mem::take(candidates), other_candidates) + .dedup() + .collect(); + } + self + } + + /// Gets the `kind` of the error. + pub fn kind(&self) -> &RevsetParseErrorKind { + &self.kind + } + + /// Original parsing error which typically occurred in an alias expression. + pub fn origin(&self) -> Option<&Self> { + self.source.as_ref().and_then(|e| e.downcast_ref()) + } +} + +impl AliasExpandError for RevsetParseError { + fn invalid_arguments(err: InvalidArguments<'_>) -> Self { + err.into() + } + + fn recursive_expansion(id: AliasId<'_>, span: pest::Span<'_>) -> Self { + Self::with_span(RevsetParseErrorKind::RecursiveAlias(id.to_string()), span) + } + + fn within_alias_expansion(self, id: AliasId<'_>, span: pest::Span<'_>) -> Self { + let kind = match id { + AliasId::Symbol(_) | AliasId::Pattern(..) | AliasId::Function(..) => { + RevsetParseErrorKind::InAliasExpansion(id.to_string()) + } + AliasId::Parameter(_) => RevsetParseErrorKind::InParameterExpansion(id.to_string()), + }; + Self::with_span(kind, span).with_source(self) + } +} + +impl From> for RevsetParseError { + fn from(err: pest::error::Error) -> Self { + Self { + kind: Box::new(RevsetParseErrorKind::SyntaxError), + pest_error: Box::new(rename_rules_in_pest_error(err)), + source: None, + } + } +} + +impl From> for RevsetParseError { + fn from(err: InvalidArguments<'_>) -> Self { + let kind = RevsetParseErrorKind::InvalidFunctionArguments { + name: err.name.to_owned(), + message: err.message, + }; + Self::with_span(kind, err.span) + } +} + +fn rename_rules_in_pest_error(mut err: pest::error::Error) -> pest::error::Error { + let pest::error::ErrorVariant::ParsingError { + positives, + negatives, + } = &mut err.variant + else { + return err; + }; + + // Remove duplicated symbols. Compat symbols are also removed from the + // (positive) suggestion. + let mut known_syms = HashSet::new(); + positives.retain(|rule| { + !rule.is_compat() && rule.to_symbol().is_none_or(|sym| known_syms.insert(sym)) + }); + let mut known_syms = HashSet::new(); + negatives.retain(|rule| rule.to_symbol().is_none_or(|sym| known_syms.insert(sym))); + err.renamed_rules(|rule| { + rule.to_symbol() + .map(|sym| format!("`{sym}`")) + .unwrap_or_else(|| format!("<{rule:?}>")) + }) +} + +/// Describes the kinds of Expressions the Revset parser understands. +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum ExpressionKind<'i> { + /// Unquoted symbol. + Identifier(&'i str), + /// Quoted symbol or string. + String(String), + /// `:` where `` is usually `Identifier` or `String`. + Pattern(Box>), + /// `@` + RemoteSymbol(RemoteRefSymbolBuf), + /// `@` + AtWorkspace(String), + /// `@` + AtCurrentWorkspace, + /// `::` + DagRangeAll, + /// `..` + RangeAll, + /// A urnary expression with its kind and expression. + Unary(UnaryOp, Box>), + /// A binary expression with its kind both primary and secondary expression. + Binary(BinaryOp, Box>, Box>), + /// `x | y | ..` + UnionAll(Vec>), + /// A function call with its specific node. + FunctionCall(Box>), + /// Identity node to preserve the span in the source text. + AliasExpanded(AliasId<'i>, Box>), +} + +impl<'i> FoldableExpression<'i> for ExpressionKind<'i> { + fn fold(self, folder: &mut F, span: pest::Span<'i>) -> Result + where + F: ExpressionFolder<'i, Self> + ?Sized, + { + match self { + Self::Identifier(name) => folder.fold_identifier(name, span), + Self::String(_) => Ok(self), + Self::Pattern(pattern) => folder.fold_pattern(pattern, span), + Self::RemoteSymbol(_) + | ExpressionKind::AtWorkspace(_) + | Self::AtCurrentWorkspace + | Self::DagRangeAll + | Self::RangeAll => Ok(self), + Self::Unary(op, arg) => { + let arg = Box::new(folder.fold_expression(*arg)?); + Ok(Self::Unary(op, arg)) + } + Self::Binary(op, lhs, rhs) => { + let lhs = Box::new(folder.fold_expression(*lhs)?); + let rhs = Box::new(folder.fold_expression(*rhs)?); + Ok(Self::Binary(op, lhs, rhs)) + } + Self::UnionAll(nodes) => { + let nodes = dsl_util::fold_expression_nodes(folder, nodes)?; + Ok(Self::UnionAll(nodes)) + } + Self::FunctionCall(function) => folder.fold_function_call(function, span), + Self::AliasExpanded(id, subst) => { + let subst = Box::new(folder.fold_expression(*subst)?); + Ok(Self::AliasExpanded(id, subst)) + } + } + } +} + +impl<'i> AliasExpandableExpression<'i> for ExpressionKind<'i> { + fn identifier(name: &'i str) -> Self { + Self::Identifier(name) + } + + fn pattern(pattern: Box>) -> Self { + Self::Pattern(pattern) + } + + fn function_call(function: Box>) -> Self { + Self::FunctionCall(function) + } + + fn alias_expanded(id: AliasId<'i>, subst: Box>) -> Self { + Self::AliasExpanded(id, subst) + } +} + +/// A urnary operation in the Revset language. +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +pub enum UnaryOp { + /// `~x` + Negate, + /// `::x` + DagRangePre, + /// `x::` + DagRangePost, + /// `..x` + RangePre, + /// `x..` + RangePost, + /// `x-` + Parents, + /// `x+` + Children, +} + +/// A binary operation in the Revset language. +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +pub enum BinaryOp { + /// `&` + Intersection, + /// `~` + Difference, + /// `::` + DagRange, + /// `..` + Range, +} + +/// A Expression in the Revset language. +pub type ExpressionNode<'i> = dsl_util::ExpressionNode<'i, ExpressionKind<'i>>; +/// A FunctionCall in the Revset language. +pub type FunctionCallNode<'i> = dsl_util::FunctionCallNode<'i, ExpressionKind<'i>>; +/// A Pattern in the Revset language. +pub type PatternNode<'i> = dsl_util::PatternNode<'i, ExpressionKind<'i>>; + +fn union_nodes<'i>(lhs: ExpressionNode<'i>, rhs: ExpressionNode<'i>) -> ExpressionNode<'i> { + let span = lhs.span.start_pos().span(&rhs.span.end_pos()); + let expr = match lhs.kind { + // Flatten "x | y | z" to save recursion stack. Machine-generated query + // might have long chain of unions. + ExpressionKind::UnionAll(mut nodes) => { + nodes.push(rhs); + ExpressionKind::UnionAll(nodes) + } + _ => ExpressionKind::UnionAll(vec![lhs, rhs]), + }; + ExpressionNode::new(expr, span) +} + +/// Parses text into expression tree. No name resolution is made at this stage. +pub fn parse_program(revset_str: &str) -> Result, RevsetParseError> { + let mut pairs = RevsetParser::parse(Rule::program, revset_str)?; + let first = pairs.next().unwrap(); + assert_eq!(first.as_rule(), Rule::expression); + parse_expression_node(first) +} + +fn parse_expression_node(pair: Pair) -> Result { + fn not_prefix_op( + op: &Pair, + similar_op: impl Into, + description: impl Into, + ) -> RevsetParseError { + RevsetParseError::with_span( + RevsetParseErrorKind::NotPrefixOperator { + op: op.as_str().to_owned(), + similar_op: similar_op.into(), + description: description.into(), + }, + op.as_span(), + ) + } + + fn not_postfix_op( + op: &Pair, + similar_op: impl Into, + description: impl Into, + ) -> RevsetParseError { + RevsetParseError::with_span( + RevsetParseErrorKind::NotPostfixOperator { + op: op.as_str().to_owned(), + similar_op: similar_op.into(), + description: description.into(), + }, + op.as_span(), + ) + } + + fn not_infix_op( + op: &Pair, + similar_op: impl Into, + description: impl Into, + ) -> RevsetParseError { + RevsetParseError::with_span( + RevsetParseErrorKind::NotInfixOperator { + op: op.as_str().to_owned(), + similar_op: similar_op.into(), + description: description.into(), + }, + op.as_span(), + ) + } + + static PRATT: LazyLock> = LazyLock::new(|| { + PrattParser::new() + .op(Op::infix(Rule::union_op, Assoc::Left) + | Op::infix(Rule::compat_add_op, Assoc::Left)) + .op(Op::infix(Rule::intersection_op, Assoc::Left) + | Op::infix(Rule::difference_op, Assoc::Left) + | Op::infix(Rule::compat_sub_op, Assoc::Left)) + .op(Op::prefix(Rule::negate_op)) + // Ranges can't be nested without parentheses. Associativity doesn't matter. + .op(Op::infix(Rule::dag_range_op, Assoc::Left) + | Op::infix(Rule::compat_dag_range_op, Assoc::Left) + | Op::infix(Rule::range_op, Assoc::Left)) + .op(Op::prefix(Rule::dag_range_pre_op) + | Op::prefix(Rule::compat_dag_range_pre_op) + | Op::prefix(Rule::range_pre_op)) + .op(Op::postfix(Rule::dag_range_post_op) + | Op::postfix(Rule::compat_dag_range_post_op) + | Op::postfix(Rule::range_post_op)) + // Neighbors + .op(Op::postfix(Rule::parents_op) + | Op::postfix(Rule::children_op) + | Op::postfix(Rule::compat_parents_op)) + }); + PRATT + .map_primary(|primary| { + let expr = match primary.as_rule() { + Rule::primary => return parse_primary_node(primary), + Rule::dag_range_all_op => ExpressionKind::DagRangeAll, + Rule::range_all_op => ExpressionKind::RangeAll, + r => panic!("unexpected primary rule {r:?}"), + }; + Ok(ExpressionNode::new(expr, primary.as_span())) + }) + .map_prefix(|op, rhs| { + let op_kind = match op.as_rule() { + Rule::negate_op => UnaryOp::Negate, + Rule::dag_range_pre_op => UnaryOp::DagRangePre, + Rule::compat_dag_range_pre_op => Err(not_prefix_op(&op, "::", "ancestors"))?, + Rule::range_pre_op => UnaryOp::RangePre, + r => panic!("unexpected prefix operator rule {r:?}"), + }; + let rhs = Box::new(rhs?); + let span = op.as_span().start_pos().span(&rhs.span.end_pos()); + let expr = ExpressionKind::Unary(op_kind, rhs); + Ok(ExpressionNode::new(expr, span)) + }) + .map_postfix(|lhs, op| { + let op_kind = match op.as_rule() { + Rule::dag_range_post_op => UnaryOp::DagRangePost, + Rule::compat_dag_range_post_op => Err(not_postfix_op(&op, "::", "descendants"))?, + Rule::range_post_op => UnaryOp::RangePost, + Rule::parents_op => UnaryOp::Parents, + Rule::children_op => UnaryOp::Children, + Rule::compat_parents_op => Err(not_postfix_op(&op, "-", "parents"))?, + r => panic!("unexpected postfix operator rule {r:?}"), + }; + let lhs = Box::new(lhs?); + let span = lhs.span.start_pos().span(&op.as_span().end_pos()); + let expr = ExpressionKind::Unary(op_kind, lhs); + Ok(ExpressionNode::new(expr, span)) + }) + .map_infix(|lhs, op, rhs| { + let op_kind = match op.as_rule() { + Rule::union_op => return Ok(union_nodes(lhs?, rhs?)), + Rule::compat_add_op => Err(not_infix_op(&op, "|", "union"))?, + Rule::intersection_op => BinaryOp::Intersection, + Rule::difference_op => BinaryOp::Difference, + Rule::compat_sub_op => Err(not_infix_op(&op, "~", "difference"))?, + Rule::dag_range_op => BinaryOp::DagRange, + Rule::compat_dag_range_op => Err(not_infix_op(&op, "::", "DAG range"))?, + Rule::range_op => BinaryOp::Range, + r => panic!("unexpected infix operator rule {r:?}"), + }; + let lhs = Box::new(lhs?); + let rhs = Box::new(rhs?); + let span = lhs.span.start_pos().span(&rhs.span.end_pos()); + let expr = ExpressionKind::Binary(op_kind, lhs, rhs); + Ok(ExpressionNode::new(expr, span)) + }) + .parse(pair.into_inner()) +} + +fn parse_primary_node(pair: Pair) -> Result { + let span = pair.as_span(); + let mut pairs = pair.into_inner(); + let first = pairs.next().unwrap(); + let expr = match first.as_rule() { + // Ignore inner span to preserve parenthesized expression as such. + Rule::expression => parse_expression_node(first)?.kind, + Rule::function => { + let function = Box::new(FUNCTION_CALL_PARSER.parse( + first, + |pair| Ok(pair.as_str()), + |pair| parse_expression_node(pair), + )?); + ExpressionKind::FunctionCall(function) + } + Rule::pattern => { + let [lhs, op, rhs] = first.into_inner().collect_array().unwrap(); + assert_eq!(lhs.as_rule(), Rule::strict_identifier); + assert_eq!(op.as_rule(), Rule::pattern_kind_op); + assert_eq!(rhs.as_rule(), Rule::pattern_value_expression); + let pattern = Box::new(PatternNode { + name: lhs.as_str(), + name_span: lhs.as_span(), + value: parse_expression_node(rhs)?, + }); + ExpressionKind::Pattern(pattern) + } + // Identifier without "@" may be substituted by aliases. Primary expression including "@" + // is considered an indecomposable unit, and no alias substitution would be made. + Rule::identifier if pairs.peek().is_none() => ExpressionKind::Identifier(first.as_str()), + Rule::identifier | Rule::string_literal | Rule::raw_string_literal => { + let name = parse_as_string_literal(first); + match pairs.next() { + None => ExpressionKind::String(name), + Some(op) => { + assert_eq!(op.as_rule(), Rule::at_op); + match pairs.next() { + // postfix "@" + None => ExpressionKind::AtWorkspace(name), + // infix "@" + Some(second) => { + let name: RefNameBuf = name.into(); + let remote: RemoteNameBuf = parse_as_string_literal(second).into(); + ExpressionKind::RemoteSymbol(RemoteRefSymbolBuf { name, remote }) + } + } + } + } + } + // nullary "@" + Rule::at_op => ExpressionKind::AtCurrentWorkspace, + r => panic!("unexpected revset parse rule: {r:?}"), + }; + Ok(ExpressionNode::new(expr, span)) +} + +/// Parses part of compound symbol to string. +fn parse_as_string_literal(pair: Pair) -> String { + match pair.as_rule() { + Rule::identifier => pair.as_str().to_owned(), + Rule::string_literal => STRING_LITERAL_PARSER.parse(pair.into_inner()), + Rule::raw_string_literal => { + let [content] = pair.into_inner().collect_array().unwrap(); + assert_eq!(content.as_rule(), Rule::raw_string_content); + content.as_str().to_owned() + } + _ => { + panic!("unexpected string literal rule: {:?}", pair.as_str()); + } + } +} + +/// Checks if the text is a valid identifier +pub fn is_identifier(text: &str) -> bool { + match RevsetParser::parse(Rule::identifier, text) { + Ok(mut pairs) => pairs.next().unwrap().as_span().end() == text.len(), + Err(_) => false, + } +} + +/// Parses the text as a revset symbol, rejects empty string. +pub fn parse_symbol(text: &str) -> Result { + let mut pairs = RevsetParser::parse(Rule::symbol_name, text)?; + let first = pairs.next().unwrap(); + let span = first.as_span(); + let name = parse_as_string_literal(first); + if name.is_empty() { + Err(RevsetParseError::expression( + "Expected non-empty string", + span, + )) + } else { + Ok(name) + } +} + +/// A map of Revset Aliases, usually defined in the `[revset-aliases]` toml +/// table. +pub type RevsetAliasesMap = AliasesMap; + +/// A RevsetAliasesParser is responsible for parsing String expressions into +/// Revset Aliases. +#[derive(Clone, Debug, Default)] +pub struct RevsetAliasParser; + +impl AliasDeclarationParser for RevsetAliasParser { + type Error = RevsetParseError; + + fn parse_declaration(&self, source: &str) -> Result { + let mut pairs = RevsetParser::parse(Rule::alias_declaration, source)?; + let first = pairs.next().unwrap(); + match first.as_rule() { + Rule::strict_identifier => Ok(AliasDeclaration::Symbol(first.as_str().to_owned())), + Rule::pattern_alias_declaration => { + let [name_pair, op, param_pair] = first.into_inner().collect_array().unwrap(); + assert_eq!(name_pair.as_rule(), Rule::strict_identifier); + assert_eq!(op.as_rule(), Rule::pattern_kind_op); + assert_eq!(param_pair.as_rule(), Rule::strict_identifier); + let name = name_pair.as_str().to_owned(); + let param = param_pair.as_str().to_owned(); + Ok(AliasDeclaration::Pattern(name, param)) + } + Rule::function_alias_declaration => { + let [name_pair, params_pair] = first.into_inner().collect_array().unwrap(); + assert_eq!(name_pair.as_rule(), Rule::function_name); + assert_eq!(params_pair.as_rule(), Rule::formal_parameters); + let name = name_pair.as_str().to_owned(); + let params_span = params_pair.as_span(); + let params = params_pair + .into_inner() + .map(|pair| match pair.as_rule() { + Rule::strict_identifier => pair.as_str().to_owned(), + r => panic!("unexpected formal parameter rule {r:?}"), + }) + .collect_vec(); + if params.iter().all_unique() { + Ok(AliasDeclaration::Function(name, params)) + } else { + Err(RevsetParseError::with_span( + RevsetParseErrorKind::RedefinedFunctionParameter, + params_span, + )) + } + } + r => panic!("unexpected alias declaration rule {r:?}"), + } + } +} + +impl AliasDefinitionParser for RevsetAliasParser { + type Output<'i> = ExpressionKind<'i>; + type Error = RevsetParseError; + + fn parse_definition<'i>(&self, source: &'i str) -> Result, Self::Error> { + parse_program(source) + } +} + +/// Expect a `StringPattern` of `type_name` in `node`. +/// Returns the parsed string and optionally the pattern name on success. +pub fn expect_string_pattern<'a>( + type_name: &str, + node: &'a ExpressionNode<'_>, +) -> Result<(&'a str, Option<&'a str>), RevsetParseError> { + catch_aliases_no_diagnostics(node, |node| match &node.kind { + ExpressionKind::Identifier(name) => Ok((*name, None)), + ExpressionKind::String(name) => Ok((name, None)), + ExpressionKind::Pattern(pattern) => { + let value = expect_string_literal("string", &pattern.value)?; + Ok((value, Some(pattern.name))) + } + _ => Err(RevsetParseError::expression( + format!("Expected {type_name}"), + node.span, + )), + }) +} + +/// Expect a literal in `node` with the given `type_name`. +pub fn expect_literal( + type_name: &str, + node: &ExpressionNode, +) -> Result { + catch_aliases_no_diagnostics(node, |node| { + let value = expect_string_literal(type_name, node)?; + value + .parse() + .map_err(|_| RevsetParseError::expression(format!("Expected {type_name}"), node.span)) + }) +} + +/// Expect a String literal in `node` with the given `type_name`. +pub fn expect_string_literal<'a>( + type_name: &str, + node: &'a ExpressionNode<'_>, +) -> Result<&'a str, RevsetParseError> { + catch_aliases_no_diagnostics(node, |node| match &node.kind { + ExpressionKind::Identifier(name) => Ok(*name), + ExpressionKind::String(name) => Ok(name), + _ => Err(RevsetParseError::expression( + format!("Expected {type_name}"), + node.span, + )), + }) +} + +/// Applies the given function to the innermost `node` by unwrapping alias +/// expansion nodes. Appends alias expansion stack to error and diagnostics. +pub fn catch_aliases<'a, 'i, T>( + diagnostics: &mut RevsetDiagnostics, + node: &'a ExpressionNode<'i>, + f: impl FnOnce(&mut RevsetDiagnostics, &'a ExpressionNode<'i>) -> Result, +) -> Result { + let (node, stack) = skip_aliases(node); + if stack.is_empty() { + f(diagnostics, node) + } else { + let mut inner_diagnostics = RevsetDiagnostics::new(); + let result = f(&mut inner_diagnostics, node); + diagnostics.extend_with(inner_diagnostics, |diag| attach_aliases_err(diag, &stack)); + result.map_err(|err| attach_aliases_err(err, &stack)) + } +} + +fn catch_aliases_no_diagnostics<'a, 'i, T>( + node: &'a ExpressionNode<'i>, + f: impl FnOnce(&'a ExpressionNode<'i>) -> Result, +) -> Result { + let (node, stack) = skip_aliases(node); + f(node).map_err(|err| attach_aliases_err(err, &stack)) +} + +fn skip_aliases<'a, 'i>( + mut node: &'a ExpressionNode<'i>, +) -> (&'a ExpressionNode<'i>, Vec<(AliasId<'i>, pest::Span<'i>)>) { + let mut stack = Vec::new(); + while let ExpressionKind::AliasExpanded(id, subst) = &node.kind { + stack.push((*id, node.span)); + node = subst; + } + (node, stack) +} + +fn attach_aliases_err( + err: RevsetParseError, + stack: &[(AliasId<'_>, pest::Span<'_>)], +) -> RevsetParseError { + stack + .iter() + .rfold(err, |err, &(id, span)| err.within_alias_expansion(id, span)) +} + +#[cfg(test)] +mod tests { + use std::collections::HashMap; + + use assert_matches::assert_matches; + + use super::*; + use crate::dsl_util::KeywordArgument; + use crate::tests::TestResult; + + #[derive(Debug)] + struct WithRevsetAliasesMap<'i> { + aliases_map: RevsetAliasesMap, + locals: HashMap<&'i str, ExpressionNode<'i>>, + } + + impl<'i> WithRevsetAliasesMap<'i> { + fn set_local(mut self, name: &'i str, value: &'i str) -> Self { + self.locals.insert(name, parse_program(value).unwrap()); + self + } + + fn parse(&'i self, text: &'i str) -> Result, RevsetParseError> { + let node = parse_program(text)?; + dsl_util::expand_aliases_with_locals(node, &self.aliases_map, &self.locals) + } + + fn parse_normalized(&'i self, text: &'i str) -> ExpressionNode<'i> { + normalize_tree(self.parse(text).unwrap()) + } + } + + fn with_aliases<'i>( + aliases: impl IntoIterator, impl Into)>, + ) -> WithRevsetAliasesMap<'i> { + let mut aliases_map = RevsetAliasesMap::new(); + for (decl, defn) in aliases { + aliases_map.insert(decl, defn, None).unwrap(); + } + WithRevsetAliasesMap { + aliases_map, + locals: HashMap::new(), + } + } + + fn parse_into_kind(text: &str) -> Result, RevsetParseErrorKind> { + parse_program(text) + .map(|node| node.kind) + .map_err(|err| *err.kind) + } + + fn parse_normalized(text: &str) -> ExpressionNode<'_> { + normalize_tree(parse_program(text).unwrap()) + } + + /// Drops auxiliary data from parsed tree so it can be compared with other. + fn normalize_tree(node: ExpressionNode) -> ExpressionNode { + fn empty_span() -> pest::Span<'static> { + pest::Span::new("", 0, 0).unwrap() + } + + fn normalize_list(nodes: Vec) -> Vec { + nodes.into_iter().map(normalize_tree).collect() + } + + fn normalize_function_call(function: FunctionCallNode) -> FunctionCallNode { + FunctionCallNode { + name: function.name, + name_span: empty_span(), + args: normalize_list(function.args), + keyword_args: function + .keyword_args + .into_iter() + .map(|arg| KeywordArgument { + name: arg.name, + name_span: empty_span(), + value: normalize_tree(arg.value), + }) + .collect(), + args_span: empty_span(), + } + } + + let normalized_kind = match node.kind { + ExpressionKind::Identifier(_) | ExpressionKind::String(_) => node.kind, + ExpressionKind::Pattern(pattern) => { + let pattern = Box::new(PatternNode { + name: pattern.name, + name_span: empty_span(), + value: normalize_tree(pattern.value), + }); + ExpressionKind::Pattern(pattern) + } + ExpressionKind::RemoteSymbol(_) + | ExpressionKind::AtWorkspace(_) + | ExpressionKind::AtCurrentWorkspace + | ExpressionKind::DagRangeAll + | ExpressionKind::RangeAll => node.kind, + ExpressionKind::Unary(op, arg) => { + let arg = Box::new(normalize_tree(*arg)); + ExpressionKind::Unary(op, arg) + } + ExpressionKind::Binary(op, lhs, rhs) => { + let lhs = Box::new(normalize_tree(*lhs)); + let rhs = Box::new(normalize_tree(*rhs)); + ExpressionKind::Binary(op, lhs, rhs) + } + ExpressionKind::UnionAll(nodes) => { + let nodes = normalize_list(nodes); + ExpressionKind::UnionAll(nodes) + } + ExpressionKind::FunctionCall(function) => { + let function = Box::new(normalize_function_call(*function)); + ExpressionKind::FunctionCall(function) + } + ExpressionKind::AliasExpanded(_, subst) => normalize_tree(*subst).kind, + }; + ExpressionNode { + kind: normalized_kind, + span: empty_span(), + } + } + + #[test] + fn test_parse_tree_eq() { + assert_eq!( + parse_normalized(r#" foo( x ) | ~bar:"baz" "#), + parse_normalized(r#"(foo(x))|(~(bar:"baz"))"#) + ); + assert_ne!(parse_normalized(r#" foo "#), parse_normalized(r#" "foo" "#)); + } + + #[test] + fn test_parse_revset() -> TestResult { + // Parse a quoted symbol + assert_eq!( + parse_into_kind("\"foo\""), + Ok(ExpressionKind::String("foo".to_owned())) + ); + assert_eq!( + parse_into_kind("'foo'"), + Ok(ExpressionKind::String("foo".to_owned())) + ); + // Parse the "parents" operator + assert_matches!( + parse_into_kind("foo-"), + Ok(ExpressionKind::Unary(UnaryOp::Parents, _)) + ); + // Parse the "children" operator + assert_matches!( + parse_into_kind("foo+"), + Ok(ExpressionKind::Unary(UnaryOp::Children, _)) + ); + // Parse the "ancestors" operator + assert_matches!( + parse_into_kind("::foo"), + Ok(ExpressionKind::Unary(UnaryOp::DagRangePre, _)) + ); + // Parse the "descendants" operator + assert_matches!( + parse_into_kind("foo::"), + Ok(ExpressionKind::Unary(UnaryOp::DagRangePost, _)) + ); + // Parse the "dag range" operator + assert_matches!( + parse_into_kind("foo::bar"), + Ok(ExpressionKind::Binary(BinaryOp::DagRange, _, _)) + ); + // Parse the nullary "dag range" operator + assert_matches!(parse_into_kind("::"), Ok(ExpressionKind::DagRangeAll)); + // Parse the "range" prefix operator + assert_matches!( + parse_into_kind("..foo"), + Ok(ExpressionKind::Unary(UnaryOp::RangePre, _)) + ); + assert_matches!( + parse_into_kind("foo.."), + Ok(ExpressionKind::Unary(UnaryOp::RangePost, _)) + ); + assert_matches!( + parse_into_kind("foo..bar"), + Ok(ExpressionKind::Binary(BinaryOp::Range, _, _)) + ); + // Parse the nullary "range" operator + assert_matches!(parse_into_kind(".."), Ok(ExpressionKind::RangeAll)); + // Parse the "negate" operator + assert_matches!( + parse_into_kind("~ foo"), + Ok(ExpressionKind::Unary(UnaryOp::Negate, _)) + ); + assert_eq!( + parse_normalized("~ ~~ foo"), + parse_normalized("~(~(~(foo)))"), + ); + // Parse the "intersection" operator + assert_matches!( + parse_into_kind("foo & bar"), + Ok(ExpressionKind::Binary(BinaryOp::Intersection, _, _)) + ); + // Parse the "union" operator + assert_matches!( + parse_into_kind("foo | bar"), + Ok(ExpressionKind::UnionAll(nodes)) if nodes.len() == 2 + ); + assert_matches!( + parse_into_kind("foo | bar | baz"), + Ok(ExpressionKind::UnionAll(nodes)) if nodes.len() == 3 + ); + // Parse the "difference" operator + assert_matches!( + parse_into_kind("foo ~ bar"), + Ok(ExpressionKind::Binary(BinaryOp::Difference, _, _)) + ); + // Parentheses are allowed before suffix operators + assert_eq!(parse_normalized("(foo)-"), parse_normalized("foo-")); + // Space is allowed around expressions + assert_eq!(parse_normalized(" ::foo "), parse_normalized("::foo")); + assert_eq!(parse_normalized("( ::foo )"), parse_normalized("::foo")); + // Space is not allowed around prefix operators + assert_eq!( + parse_into_kind(" :: foo "), + Err(RevsetParseErrorKind::SyntaxError) + ); + // Incomplete parse + assert_eq!( + parse_into_kind("foo | -"), + Err(RevsetParseErrorKind::SyntaxError) + ); + + // Expression span + assert_eq!(parse_program(" ~ x ")?.span.as_str(), "~ x"); + assert_eq!(parse_program(" x+ ")?.span.as_str(), "x+"); + assert_eq!(parse_program(" x |y ")?.span.as_str(), "x |y"); + assert_eq!(parse_program(" (x) ")?.span.as_str(), "(x)"); + assert_eq!(parse_program("~( x|y) ")?.span.as_str(), "~( x|y)"); + assert_eq!(parse_program(" ( x )- ")?.span.as_str(), "( x )-"); + Ok(()) + } + + #[test] + fn test_parse_whitespace() { + let ascii_whitespaces: String = ('\x00'..='\x7f') + .filter(char::is_ascii_whitespace) + .collect(); + assert_eq!( + parse_normalized(&format!("{ascii_whitespaces}all()")), + parse_normalized("all()"), + ); + } + + #[test] + fn test_parse_identifier() { + // Integer is a symbol + assert_eq!(parse_into_kind("0"), Ok(ExpressionKind::Identifier("0"))); + // Tag/bookmark name separated by / + assert_eq!( + parse_into_kind("foo_bar/baz"), + Ok(ExpressionKind::Identifier("foo_bar/baz")) + ); + // Glob literal with star + assert_eq!( + parse_into_kind("*/foo/**"), + Ok(ExpressionKind::Identifier("*/foo/**")) + ); + + // Internal '.', '-', and '+' are allowed + assert_eq!( + parse_into_kind("foo.bar-v1+7"), + Ok(ExpressionKind::Identifier("foo.bar-v1+7")) + ); + assert_eq!( + parse_normalized("foo.bar-v1+7-"), + parse_normalized("(foo.bar-v1+7)-") + ); + // Multiple '-' are allowed + assert_eq!( + parse_into_kind("foo--bar"), + Ok(ExpressionKind::Identifier("foo--bar")) + ); + assert_eq!( + parse_into_kind("foo----bar"), + Ok(ExpressionKind::Identifier("foo----bar")) + ); + // '.' is not allowed at the beginning or end + assert_eq!( + parse_into_kind(".foo"), + Err(RevsetParseErrorKind::SyntaxError) + ); + assert_eq!( + parse_into_kind("foo."), + Err(RevsetParseErrorKind::SyntaxError) + ); + // Multiple '.' and '+', or together with '-', are not allowed + assert_eq!( + parse_into_kind("foo.+bar"), + Err(RevsetParseErrorKind::SyntaxError) + ); + assert_eq!( + parse_into_kind("foo++bar"), + Err(RevsetParseErrorKind::SyntaxError) + ); + assert_eq!( + parse_into_kind("foo+-bar"), + Err(RevsetParseErrorKind::SyntaxError) + ); + + // Parse a parenthesized symbol + assert_eq!(parse_normalized("(foo)"), parse_normalized("foo")); + + // Non-ASCII tag/bookmark name + assert_eq!( + parse_into_kind("柔術+jj"), + Ok(ExpressionKind::Identifier("柔術+jj")) + ); + } + + #[test] + fn test_parse_string_literal() { + // "\" escapes + assert_eq!( + parse_into_kind(r#" "\t\r\n\"\\\0\e" "#), + Ok(ExpressionKind::String("\t\r\n\"\\\0\u{1b}".to_owned())) + ); + + // Invalid "\" escape + assert_eq!( + parse_into_kind(r#" "\y" "#), + Err(RevsetParseErrorKind::SyntaxError) + ); + + // Single-quoted raw string + assert_eq!( + parse_into_kind(r#" '' "#), + Ok(ExpressionKind::String("".to_owned())) + ); + assert_eq!( + parse_into_kind(r#" 'a\n' "#), + Ok(ExpressionKind::String(r"a\n".to_owned())) + ); + assert_eq!( + parse_into_kind(r#" '\' "#), + Ok(ExpressionKind::String(r"\".to_owned())) + ); + assert_eq!( + parse_into_kind(r#" '"' "#), + Ok(ExpressionKind::String(r#"""#.to_owned())) + ); + + // Hex bytes + assert_eq!( + parse_into_kind(r#""\x61\x65\x69\x6f\x75""#), + Ok(ExpressionKind::String("aeiou".to_owned())) + ); + assert_eq!( + parse_into_kind(r#""\xe0\xe8\xec\xf0\xf9""#), + Ok(ExpressionKind::String("àèìðù".to_owned())) + ); + assert_eq!( + parse_into_kind(r#""\x""#), + Err(RevsetParseErrorKind::SyntaxError) + ); + assert_eq!( + parse_into_kind(r#""\xf""#), + Err(RevsetParseErrorKind::SyntaxError) + ); + assert_eq!( + parse_into_kind(r#""\xgg""#), + Err(RevsetParseErrorKind::SyntaxError) + ); + } + + #[test] + fn test_parse_pattern() -> TestResult { + fn unwrap_pattern(kind: ExpressionKind<'_>) -> (&str, ExpressionKind<'_>) { + match kind { + ExpressionKind::Pattern(pattern) => (pattern.name, pattern.value.kind), + _ => panic!("unexpected expression: {kind:?}"), + } + } + + assert_eq!( + unwrap_pattern(parse_into_kind(r#"substring:"foo""#)?), + ("substring", ExpressionKind::String("foo".to_owned())) + ); + assert_eq!( + unwrap_pattern(parse_into_kind("exact:foo")?), + ("exact", ExpressionKind::Identifier("foo")) + ); + assert_eq!( + parse_into_kind(r#""exact:foo""#), + Ok(ExpressionKind::String("exact:foo".to_owned())) + ); + // Symbol-like value expressions + assert_eq!( + unwrap_pattern(parse_into_kind("x:@")?), + ("x", ExpressionKind::AtCurrentWorkspace) + ); + assert_eq!( + unwrap_pattern(parse_into_kind("x:y@z")?), + ( + "x", + ExpressionKind::RemoteSymbol(RemoteRefSymbolBuf { + name: "y".into(), + remote: "z".into(), + }) + ) + ); + + assert_eq!( + parse_normalized(r#"(exact:"foo" )"#), + parse_normalized(r#"(exact:"foo")"#), + ); + assert_eq!( + unwrap_pattern(parse_into_kind(r#"exact:'\'"#)?), + ("exact", ExpressionKind::String(r"\".to_owned())) + ); + + // Whitespace isn't allowed in between + assert_matches!( + parse_into_kind("exact: foo"), + Err(RevsetParseErrorKind::SyntaxError) + ); + assert_matches!( + parse_into_kind("exact :foo"), + Err(RevsetParseErrorKind::SyntaxError) + ); + // Whitespace is allowed in parenthesized value expression + assert_eq!( + parse_normalized("exact:( 'foo' )"), + parse_normalized("exact:'foo'"), + ); + + // Functions are allowed + assert_eq!(parse_normalized("x:f(y)"), parse_normalized("x:(f(y))")); + // Neighbor postfix operations are also allowed + assert_eq!(parse_normalized("x:@-+"), parse_normalized("x:((@-)+)")); + // Ranges have lower binding strength because we wouldn't want to parse + // x::: as x:(::) + assert_eq!(parse_normalized("x:y::z"), parse_normalized("(x:y)::(z)")); + assert_matches!( + parse_into_kind("x:::"), + Err(RevsetParseErrorKind::SyntaxError) + ); + // Logical operators have lower binding strength + assert_eq!(parse_normalized("x:y&z"), parse_normalized("(x:y)&(z)")); + assert_matches!( + parse_into_kind("x:~y"), // (x:) ~ (y) + Err(RevsetParseErrorKind::NotPostfixOperator { .. }) + ); + + // Pattern prefix is like (type)x cast, so is evaluated from right + assert_eq!(parse_normalized("x:y:z"), parse_normalized("x:(y:z)")); + Ok(()) + } + + #[test] + fn test_parse_symbol_explicitly() { + assert_matches!(parse_symbol("").as_deref(), Err(_)); + // empty string could be a valid ref name, but it would be super + // confusing if identifier was empty. + assert_matches!(parse_symbol("''").as_deref(), Err(_)); + + assert_matches!(parse_symbol("foo.bar").as_deref(), Ok("foo.bar")); + assert_matches!(parse_symbol("foo@bar").as_deref(), Err(_)); + assert_matches!(parse_symbol("foo bar").as_deref(), Err(_)); + + assert_matches!(parse_symbol("'foo bar'").as_deref(), Ok("foo bar")); + assert_matches!(parse_symbol(r#""foo\tbar""#).as_deref(), Ok("foo\tbar")); + + // leading/trailing whitespace is NOT ignored. + assert_matches!(parse_symbol(" foo").as_deref(), Err(_)); + assert_matches!(parse_symbol("foo ").as_deref(), Err(_)); + + // (foo) could be parsed as a symbol "foo", but is rejected because user + // might expect a literal "(foo)". + assert_matches!(parse_symbol("(foo)").as_deref(), Err(_)); + } + + #[test] + fn parse_at_workspace_and_remote_symbol() { + // Parse "@" (the current working copy) + assert_eq!(parse_into_kind("@"), Ok(ExpressionKind::AtCurrentWorkspace)); + assert_eq!( + parse_into_kind("main@"), + Ok(ExpressionKind::AtWorkspace("main".to_owned())) + ); + assert_eq!( + parse_into_kind("main@origin"), + Ok(ExpressionKind::RemoteSymbol(RemoteRefSymbolBuf { + name: "main".into(), + remote: "origin".into() + })) + ); + + // Quoted component in @ expression + assert_eq!( + parse_into_kind(r#""foo bar"@"#), + Ok(ExpressionKind::AtWorkspace("foo bar".to_owned())) + ); + assert_eq!( + parse_into_kind(r#""foo bar"@origin"#), + Ok(ExpressionKind::RemoteSymbol(RemoteRefSymbolBuf { + name: "foo bar".into(), + remote: "origin".into() + })) + ); + assert_eq!( + parse_into_kind(r#"main@"foo bar""#), + Ok(ExpressionKind::RemoteSymbol(RemoteRefSymbolBuf { + name: "main".into(), + remote: "foo bar".into() + })) + ); + assert_eq!( + parse_into_kind(r#"'foo bar'@'bar baz'"#), + Ok(ExpressionKind::RemoteSymbol(RemoteRefSymbolBuf { + name: "foo bar".into(), + remote: "bar baz".into() + })) + ); + + // Quoted "@" is not interpreted as a working copy or remote symbol + assert_eq!( + parse_into_kind(r#""@""#), + Ok(ExpressionKind::String("@".to_owned())) + ); + assert_eq!( + parse_into_kind(r#""main@""#), + Ok(ExpressionKind::String("main@".to_owned())) + ); + assert_eq!( + parse_into_kind(r#""main@origin""#), + Ok(ExpressionKind::String("main@origin".to_owned())) + ); + + // Non-ASCII name + assert_eq!( + parse_into_kind("柔術@"), + Ok(ExpressionKind::AtWorkspace("柔術".to_owned())) + ); + assert_eq!( + parse_into_kind("柔@術"), + Ok(ExpressionKind::RemoteSymbol(RemoteRefSymbolBuf { + name: "柔".into(), + remote: "術".into() + })) + ); + } + + #[test] + fn test_parse_function_call() -> TestResult { + fn unwrap_function_call(node: ExpressionNode<'_>) -> Box> { + match node.kind { + ExpressionKind::FunctionCall(function) => function, + _ => panic!("unexpected expression: {node:?}"), + } + } + + // Space is allowed around infix operators and function arguments + assert_eq!( + parse_normalized( + " description( arg1 ) ~ file( arg1 , arg2 ) ~ visible_heads( ) ", + ), + parse_normalized("(description(arg1) ~ file(arg1, arg2)) ~ visible_heads()"), + ); + // Space is allowed around keyword arguments + assert_eq!( + parse_normalized("remote_bookmarks( remote = foo )"), + parse_normalized("remote_bookmarks(remote=foo)"), + ); + + // Trailing comma isn't allowed for empty argument + assert!(parse_into_kind("bookmarks(,)").is_err()); + // Trailing comma is allowed for the last argument + assert_eq!( + parse_normalized("bookmarks(a,)"), + parse_normalized("bookmarks(a)") + ); + assert_eq!( + parse_normalized("bookmarks(a , )"), + parse_normalized("bookmarks(a)") + ); + assert!(parse_into_kind("bookmarks(,a)").is_err()); + assert!(parse_into_kind("bookmarks(a,,)").is_err()); + assert!(parse_into_kind("bookmarks(a , , )").is_err()); + assert_eq!( + parse_normalized("file(a,b,)"), + parse_normalized("file(a, b)") + ); + assert!(parse_into_kind("file(a,,b)").is_err()); + assert_eq!( + parse_normalized("remote_bookmarks(a,remote=b , )"), + parse_normalized("remote_bookmarks(a, remote=b)"), + ); + assert!(parse_into_kind("remote_bookmarks(a,,remote=b)").is_err()); + + // Expression span + let function = unwrap_function_call(parse_program("foo( a, (b) , ~(c), d = (e) )")?); + assert_eq!(function.name_span.as_str(), "foo"); + assert_eq!(function.args_span.as_str(), "a, (b) , ~(c), d = (e)"); + assert_eq!(function.args[0].span.as_str(), "a"); + assert_eq!(function.args[1].span.as_str(), "(b)"); + assert_eq!(function.args[2].span.as_str(), "~(c)"); + assert_eq!(function.keyword_args[0].name_span.as_str(), "d"); + assert_eq!(function.keyword_args[0].value.span.as_str(), "(e)"); + Ok(()) + } + + #[test] + fn test_parse_revset_alias_symbol_decl() { + let mut aliases_map = RevsetAliasesMap::new(); + // Working copy or remote symbol cannot be used as an alias name. + assert!(aliases_map.insert("@", "none()", None).is_err()); + assert!(aliases_map.insert("a@", "none()", None).is_err()); + assert!(aliases_map.insert("a@b", "none()", None).is_err()); + // Non-ASCII character isn't allowed in alias symbol. This rule can be + // relaxed if needed. + assert!(aliases_map.insert("柔術", "none()", None).is_err()); + } + + #[test] + fn test_parse_revset_alias_pattern_decl() -> TestResult { + let mut aliases_map = RevsetAliasesMap::new(); + assert!(aliases_map.insert("foo:", "none()", None).is_err()); + assert_eq!(aliases_map.pattern_names().count(), 0); + + aliases_map.insert("bar:baz", "'bar pattern'", None)?; + assert_eq!(aliases_map.pattern_names().count(), 1); + let (id, param, defn, _doc) = aliases_map.get_pattern("bar").unwrap(); + assert_eq!(id, AliasId::Pattern("bar", "baz")); + assert_eq!(param, "baz"); + assert_eq!(defn, "'bar pattern'"); + + // Non-ASCII character isn't allowed. This rule can be relaxed if + // needed. + assert!(aliases_map.insert("柔術:x", "none()", None).is_err()); + assert!(aliases_map.insert("x:柔術", "none()", None).is_err()); + Ok(()) + } + + #[test] + fn test_parse_revset_alias_func_decl() -> TestResult { + let mut aliases_map = RevsetAliasesMap::new(); + assert!( + aliases_map + .insert("5func()", r#""is function 0""#, None) + .is_err() + ); + aliases_map.insert("func()", r#""is function 0""#, None)?; + aliases_map.insert("func(a, b)", r#""is function 2""#, None)?; + aliases_map.insert("func(a)", r#""is function a""#, None)?; + aliases_map.insert("func(b)", r#""is function b""#, None)?; + + let (id, params, defn, _doc) = aliases_map.get_function("func", 0).unwrap(); + assert_eq!(id, AliasId::Function("func", &[])); + assert!(params.is_empty()); + assert_eq!(defn, r#""is function 0""#); + + let (id, params, defn, _doc) = aliases_map.get_function("func", 1).unwrap(); + assert_eq!(id, AliasId::Function("func", &["b".to_owned()])); + assert_eq!(params, ["b"]); + assert_eq!(defn, r#""is function b""#); + + let (id, params, defn, _doc) = aliases_map.get_function("func", 2).unwrap(); + assert_eq!( + id, + AliasId::Function("func", &["a".to_owned(), "b".to_owned()]) + ); + assert_eq!(params, ["a", "b"]); + assert_eq!(defn, r#""is function 2""#); + + assert!(aliases_map.get_function("func", 3).is_none()); + Ok(()) + } + + #[test] + fn test_parse_revset_alias_formal_parameter() { + let mut aliases_map = RevsetAliasesMap::new(); + // Working copy or remote symbol cannot be used as an parameter name. + assert!(aliases_map.insert("f(@)", "none()", None).is_err()); + assert!(aliases_map.insert("f(a@)", "none()", None).is_err()); + assert!(aliases_map.insert("f(a@b)", "none()", None).is_err()); + // Trailing comma isn't allowed for empty parameter + assert!(aliases_map.insert("f(,)", "none()", None).is_err()); + // Trailing comma is allowed for the last parameter + assert!(aliases_map.insert("g(a,)", "none()", None).is_ok()); + assert!(aliases_map.insert("h(a , )", "none()", None).is_ok()); + assert!(aliases_map.insert("i(,a)", "none()", None).is_err()); + assert!(aliases_map.insert("j(a,,)", "none()", None).is_err()); + assert!(aliases_map.insert("k(a , , )", "none()", None).is_err()); + assert!(aliases_map.insert("l(a,b,)", "none()", None).is_ok()); + assert!(aliases_map.insert("m(a,,b)", "none()", None).is_err()); + } + + #[test] + fn test_parse_revset_compat_operator() { + assert_eq!( + parse_into_kind(":foo"), + Err(RevsetParseErrorKind::NotPrefixOperator { + op: ":".to_owned(), + similar_op: "::".to_owned(), + description: "ancestors".to_owned(), + }) + ); + assert_eq!( + parse_into_kind("foo^"), + Err(RevsetParseErrorKind::NotPostfixOperator { + op: "^".to_owned(), + similar_op: "-".to_owned(), + description: "parents".to_owned(), + }) + ); + assert_eq!( + parse_into_kind("foo + bar"), + Err(RevsetParseErrorKind::NotInfixOperator { + op: "+".to_owned(), + similar_op: "|".to_owned(), + description: "union".to_owned(), + }) + ); + assert_eq!( + parse_into_kind("foo - bar"), + Err(RevsetParseErrorKind::NotInfixOperator { + op: "-".to_owned(), + similar_op: "~".to_owned(), + description: "difference".to_owned(), + }) + ); + } + + #[test] + fn test_parse_revset_operator_combinations() { + // Parse repeated "parents" operator + assert_eq!(parse_normalized("foo---"), parse_normalized("((foo-)-)-")); + // Parse repeated "children" operator + assert_eq!(parse_normalized("foo+++"), parse_normalized("((foo+)+)+")); + // Set operator associativity/precedence + assert_eq!(parse_normalized("~x|y"), parse_normalized("(~x)|y")); + assert_eq!(parse_normalized("x&~y"), parse_normalized("x&(~y)")); + assert_eq!(parse_normalized("x~~y"), parse_normalized("x~(~y)")); + assert_eq!(parse_normalized("x~~~y"), parse_normalized("x~(~(~y))")); + assert_eq!(parse_normalized("~x::y"), parse_normalized("~(x::y)")); + assert_eq!(parse_normalized("x|y|z"), parse_normalized("(x|y)|z")); + assert_eq!(parse_normalized("x&y|z"), parse_normalized("(x&y)|z")); + assert_eq!(parse_normalized("x|y&z"), parse_normalized("x|(y&z)")); + assert_eq!(parse_normalized("x|y~z"), parse_normalized("x|(y~z)")); + assert_eq!(parse_normalized("::&.."), parse_normalized("(::)&(..)")); + // Parse repeated "ancestors"/"descendants"/"dag range"/"range" operators + assert_eq!( + parse_into_kind("::foo::"), + Err(RevsetParseErrorKind::SyntaxError) + ); + assert_eq!( + parse_into_kind(":::foo"), + Err(RevsetParseErrorKind::SyntaxError) + ); + assert_eq!( + parse_into_kind("::::foo"), + Err(RevsetParseErrorKind::SyntaxError) + ); + assert_eq!( + parse_into_kind("foo:::"), + Err(RevsetParseErrorKind::SyntaxError) + ); + assert_eq!( + parse_into_kind("foo::::"), + Err(RevsetParseErrorKind::SyntaxError) + ); + assert_eq!( + parse_into_kind("foo:::bar"), + Err(RevsetParseErrorKind::SyntaxError) + ); + assert_eq!( + parse_into_kind("foo::::bar"), + Err(RevsetParseErrorKind::SyntaxError) + ); + assert_eq!( + parse_into_kind("::foo::bar"), + Err(RevsetParseErrorKind::SyntaxError) + ); + assert_eq!( + parse_into_kind("foo::bar::"), + Err(RevsetParseErrorKind::SyntaxError) + ); + assert_eq!( + parse_into_kind("::::"), + Err(RevsetParseErrorKind::SyntaxError) + ); + assert_eq!( + parse_into_kind("....foo"), + Err(RevsetParseErrorKind::SyntaxError) + ); + assert_eq!( + parse_into_kind("foo...."), + Err(RevsetParseErrorKind::SyntaxError) + ); + assert_eq!( + parse_into_kind("foo.....bar"), + Err(RevsetParseErrorKind::SyntaxError) + ); + assert_eq!( + parse_into_kind("..foo..bar"), + Err(RevsetParseErrorKind::SyntaxError) + ); + assert_eq!( + parse_into_kind("foo..bar.."), + Err(RevsetParseErrorKind::SyntaxError) + ); + assert_eq!( + parse_into_kind("...."), + Err(RevsetParseErrorKind::SyntaxError) + ); + assert_eq!( + parse_into_kind("::.."), + Err(RevsetParseErrorKind::SyntaxError) + ); + // Parse combinations of "parents"/"children" operators and the range operators. + // The former bind more strongly. + assert_eq!(parse_normalized("foo-+"), parse_normalized("(foo-)+")); + assert_eq!(parse_normalized("foo-::"), parse_normalized("(foo-)::")); + assert_eq!(parse_normalized("::foo+"), parse_normalized("::(foo+)")); + assert_eq!( + parse_into_kind("::-"), + Err(RevsetParseErrorKind::SyntaxError) + ); + assert_eq!( + parse_into_kind("..+"), + Err(RevsetParseErrorKind::SyntaxError) + ); + } + + #[test] + fn test_parse_revset_function() { + assert_matches!( + parse_into_kind("parents(foo)"), + Ok(ExpressionKind::FunctionCall(_)) + ); + assert_eq!( + parse_normalized("parents((foo))"), + parse_normalized("parents(foo)"), + ); + assert_eq!( + parse_into_kind("parents(foo"), + Err(RevsetParseErrorKind::SyntaxError) + ); + } + + #[test] + fn test_expand_symbol_alias() { + assert_eq!( + with_aliases([("AB", "a&b")]).parse_normalized("AB|c"), + parse_normalized("(a&b)|c") + ); + assert_eq!( + with_aliases([("AB", "a|b")]).parse_normalized("AB::heads(AB)"), + parse_normalized("(a|b)::heads(a|b)") + ); + + // Not string substitution 'a&b|c', but tree substitution. + assert_eq!( + with_aliases([("BC", "b|c")]).parse_normalized("a&BC"), + parse_normalized("a&(b|c)") + ); + + // String literal should not be substituted with alias. + assert_eq!( + with_aliases([("A", "a")]).parse_normalized(r#"A|"A"|'A'"#), + parse_normalized("a|'A'|'A'") + ); + + // Kind of string pattern should not be substituted, which is similar to + // function name. + assert_eq!( + with_aliases([("A", "a")]).parse_normalized("author(A:b)"), + parse_normalized("author(A:b)") + ); + + // Value of string pattern can be substituted if it's an identifier. + assert_eq!( + with_aliases([("A", "a")]).parse_normalized("author(exact:A)"), + parse_normalized("author(exact:a)") + ); + assert_eq!( + with_aliases([("A", "a")]).parse_normalized("author(exact:'A')"), + parse_normalized("author(exact:'A')") + ); + + // Part of @ symbol cannot be substituted. + assert_eq!( + with_aliases([("A", "a")]).parse_normalized("A@"), + parse_normalized("A@") + ); + assert_eq!( + with_aliases([("A", "a")]).parse_normalized("A@b"), + parse_normalized("A@b") + ); + assert_eq!( + with_aliases([("B", "b")]).parse_normalized("a@B"), + parse_normalized("a@B") + ); + + // Multi-level substitution. + assert_eq!( + with_aliases([("A", "BC"), ("BC", "b|C"), ("C", "c")]).parse_normalized("A"), + parse_normalized("b|c") + ); + + // Infinite recursion, where the top-level error isn't of RecursiveAlias kind. + assert_eq!( + *with_aliases([("A", "A")]).parse("A").unwrap_err().kind, + RevsetParseErrorKind::InAliasExpansion("A".to_owned()) + ); + assert_eq!( + *with_aliases([("A", "B"), ("B", "b|C"), ("C", "c|B")]) + .parse("A") + .unwrap_err() + .kind, + RevsetParseErrorKind::InAliasExpansion("A".to_owned()) + ); + + // Error in alias definition. + assert_eq!( + *with_aliases([("A", "a(")]).parse("A").unwrap_err().kind, + RevsetParseErrorKind::InAliasExpansion("A".to_owned()) + ); + } + + #[test] + fn test_expand_pattern_alias() { + assert_eq!( + with_aliases([("P:x", "x")]).parse_normalized("P:a"), + parse_normalized("a") + ); + + // Argument should be resolved in the current scope. + assert_eq!( + with_aliases([("P:x", "x|a")]).parse_normalized("P:x"), + parse_normalized("x|a") + ); + // P:a -> (Q:a)&y -> (x|a)&y + assert_eq!( + with_aliases([("P:x", "(Q:x)&y"), ("Q:y", "x|y")]).parse_normalized("P:a"), + parse_normalized("(x|a)&y") + ); + + // Pattern parameter should precede the symbol alias. + assert_eq!( + with_aliases([("P:X", "X"), ("X", "x")]).parse_normalized("(P:a)|X"), + parse_normalized("a|x") + ); + + // Pattern parameter shouldn't be expanded in symbol alias. + assert_eq!( + with_aliases([("P:x", "x|A"), ("A", "x")]).parse_normalized("P:a"), + parse_normalized("a|x") + ); + + // String literal should not be substituted with pattern parameter. + assert_eq!( + with_aliases([("P:x", "x|'x'")]).parse_normalized("P:a"), + parse_normalized("a|'x'") + ); + + // Pattern and symbol aliases reside in separate namespaces. + assert_eq!( + with_aliases([("A:x", "A"), ("A", "a")]).parse_normalized("A:x"), + parse_normalized("a") + ); + + // Infinite recursion, where the top-level error isn't of RecursiveAlias kind. + assert_eq!( + *with_aliases([("P:x", "Q:x"), ("Q:x", "R:x"), ("R:x", "P:x")]) + .parse("P:a") + .unwrap_err() + .kind, + RevsetParseErrorKind::InAliasExpansion("P:x".to_owned()) + ); + } + + #[test] + fn test_expand_function_alias() { + assert_eq!( + with_aliases([("F( )", "a")]).parse_normalized("F()"), + parse_normalized("a") + ); + assert_eq!( + with_aliases([("F( x )", "x")]).parse_normalized("F(a)"), + parse_normalized("a") + ); + assert_eq!( + with_aliases([("F( x, y )", "x|y")]).parse_normalized("F(a, b)"), + parse_normalized("a|b") + ); + + // Not recursion because functions are overloaded by arity. + assert_eq!( + with_aliases([("F(x)", "F(x,b)"), ("F(x,y)", "x|y")]).parse_normalized("F(a)"), + parse_normalized("a|b") + ); + + // Arguments should be resolved in the current scope. + assert_eq!( + with_aliases([("F(x,y)", "x|y")]).parse_normalized("F(a::y,b::x)"), + parse_normalized("(a::y)|(b::x)") + ); + // F(a) -> G(a)&y -> (x|a)&y + assert_eq!( + with_aliases([("F(x)", "G(x)&y"), ("G(y)", "x|y")]).parse_normalized("F(a)"), + parse_normalized("(x|a)&y") + ); + // F(G(a)) -> F(x|a) -> G(x|a)&y -> (x|(x|a))&y + assert_eq!( + with_aliases([("F(x)", "G(x)&y"), ("G(y)", "x|y")]).parse_normalized("F(G(a))"), + parse_normalized("(x|(x|a))&y") + ); + + // Function parameter should precede the symbol alias. + assert_eq!( + with_aliases([("F(X)", "X"), ("X", "x")]).parse_normalized("F(a)|X"), + parse_normalized("a|x") + ); + + // Function parameter shouldn't be expanded in symbol alias. + assert_eq!( + with_aliases([("F(x)", "x|A"), ("A", "x")]).parse_normalized("F(a)"), + parse_normalized("a|x") + ); + + // String literal should not be substituted with function parameter. + assert_eq!( + with_aliases([("F(x)", r#"x|"x""#)]).parse_normalized("F(a)"), + parse_normalized("a|'x'") + ); + + // Function and symbol aliases reside in separate namespaces. + assert_eq!( + with_aliases([("A()", "A"), ("A", "a")]).parse_normalized("A()"), + parse_normalized("a") + ); + + // Invalid number of arguments. + assert_eq!( + *with_aliases([("F()", "x")]).parse("F(a)").unwrap_err().kind, + RevsetParseErrorKind::InvalidFunctionArguments { + name: "F".to_owned(), + message: "Expected 0 arguments".to_owned() + } + ); + assert_eq!( + *with_aliases([("F(x)", "x")]).parse("F()").unwrap_err().kind, + RevsetParseErrorKind::InvalidFunctionArguments { + name: "F".to_owned(), + message: "Expected 1 arguments".to_owned() + } + ); + assert_eq!( + *with_aliases([("F(x,y)", "x|y")]) + .parse("F(a,b,c)") + .unwrap_err() + .kind, + RevsetParseErrorKind::InvalidFunctionArguments { + name: "F".to_owned(), + message: "Expected 2 arguments".to_owned() + } + ); + assert_eq!( + *with_aliases([("F(x)", "x"), ("F(x,y)", "x|y")]) + .parse("F()") + .unwrap_err() + .kind, + RevsetParseErrorKind::InvalidFunctionArguments { + name: "F".to_owned(), + message: "Expected 1 to 2 arguments".to_owned() + } + ); + assert_eq!( + *with_aliases([("F()", "x"), ("F(x,y)", "x|y")]) + .parse("F(a)") + .unwrap_err() + .kind, + RevsetParseErrorKind::InvalidFunctionArguments { + name: "F".to_owned(), + message: "Expected 0, 2 arguments".to_owned() + } + ); + + // Keyword argument isn't supported for now. + assert_eq!( + *with_aliases([("F(x)", "x")]) + .parse("F(x=y)") + .unwrap_err() + .kind, + RevsetParseErrorKind::InvalidFunctionArguments { + name: "F".to_owned(), + message: "Unexpected keyword arguments".to_owned() + } + ); + + // Infinite recursion, where the top-level error isn't of RecursiveAlias kind. + assert_eq!( + *with_aliases([("F(x)", "G(x)"), ("G(x)", "H(x)"), ("H(x)", "F(x)")]) + .parse("F(a)") + .unwrap_err() + .kind, + RevsetParseErrorKind::InAliasExpansion("F(x)".to_owned()) + ); + assert_eq!( + *with_aliases([("F(x)", "F(x,b)"), ("F(x,y)", "F(x|y)")]) + .parse("F(a)") + .unwrap_err() + .kind, + RevsetParseErrorKind::InAliasExpansion("F(x)".to_owned()) + ); + } + + #[test] + fn test_expand_with_locals() { + // Local variable should precede the symbol alias. + assert_eq!( + with_aliases([("A", "symbol")]) + .set_local("A", "local") + .parse_normalized("A"), + parse_normalized("local") + ); + + // Local variable shouldn't be expanded within aliases. + assert_eq!( + with_aliases([("B", "A"), ("F(x)", "x&A")]) + .set_local("A", "a") + .parse_normalized("A|B|F(A)"), + parse_normalized("a|A|(a&A)") + ); + } +} diff --git a/lib/core/src/signing.rs b/lib/core/src/signing.rs new file mode 100644 index 00000000000..8ba8fa76987 --- /dev/null +++ b/lib/core/src/signing.rs @@ -0,0 +1,220 @@ +// Copyright 2026 The Jujutsu Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Contains the [`SigningBackend`] a trait required for various signing +//! interactions. + +use std::fmt::Debug; +use std::fmt::Display; +use std::num::NonZeroUsize; +use std::sync::Mutex; + +use clru::CLruCache; +use thiserror::Error; + +use crate::backend::CommitId; + +// TODO: This is a duplication of `jj_lib::store::COMMIT_CACHE_CAPACITY`. Use +// the respective constant when we lower `Store`. +const SIGN_CACHE_CAPACITY: NonZeroUsize = NonZeroUsize::new(100).unwrap(); + +/// A status of the signature, part of the [Verification] type. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum SigStatus { + /// Valid signature that matches the data. + Good, + /// Valid signature that could not be verified (e.g. due to an unknown key). + Unknown, + /// Valid signature that does not match the signed data. + Bad, +} + +impl Display for SigStatus { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let s = match self { + Self::Good => "good", + Self::Unknown => "unknown", + Self::Bad => "bad", + }; + write!(f, "{s}") + } +} + +/// The result of a signature verification. +/// Key and display are optional additional info that backends can or can not +/// provide to add additional information for the templater to potentially show. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Verification { + /// The status of the signature. + pub status: SigStatus, + /// The key id representation, if available. For GPG, this will be the key + /// fingerprint. + pub key: Option, + /// A display string, if available. For GPG, this will be formatted primary + /// user ID. + pub display: Option, +} + +impl Verification { + /// A shortcut to create an `Unknown` verification with no additional + /// metadata. + pub fn unknown() -> Self { + Self { + status: SigStatus::Unknown, + key: None, + display: None, + } + } + + /// Create a new verification + pub fn new(status: SigStatus, key: Option, display: Option) -> Self { + Self { + status, + key, + display, + } + } +} + +/// Wraps low-level signing backends and adds caching, similar to `Store`. +#[derive(Debug)] +pub struct Signer { + /// The backend that is used for signing commits. + /// Optional because signing might not be configured. + main_backend: Option>, + /// All known backends without the main one - used for verification. + /// Main backend is also used for verification, but it's not in this list + /// for ownership reasons. + backends: Vec>, + cache: Mutex>, +} + +impl Signer { + /// Creates a signer with the given backends. + pub fn new( + main_backend: Option>, + other_backends: Vec>, + ) -> Self { + Self { + main_backend, + backends: other_backends, + cache: Mutex::new(CLruCache::new(SIGN_CACHE_CAPACITY)), + } + } + + /// Checks if the signer can sign, i.e. if a main backend is configured. + pub fn can_sign(&self) -> bool { + self.main_backend.is_some() + } + + /// This is just a pass-through to the main backend that unconditionally + /// creates a signature. + pub fn sign(&self, data: &[u8], key: Option<&str>) -> SignResult> { + self.main_backend + .as_ref() + .expect("tried to sign without checking can_sign first") + .sign(data, key) + } + + /// Looks for backend that can verify the signature and returns the result + /// of its verification. + pub fn verify( + &self, + commit_id: &CommitId, + data: &[u8], + signature: &[u8], + ) -> SignResult { + let cached = self.cache.lock().unwrap().get(commit_id).cloned(); + if let Some(check) = cached { + return Ok(check); + } + + let verification = self + .main_backend + .iter() + .chain(self.backends.iter()) + .filter(|b| b.can_read(signature)) + // skip unknown and invalid sigs to allow other backends that can read to try + // for example, we might have gpg and sq, both of which could read a PGP signature + .find_map(|backend| match backend.verify(data, signature) { + Ok(check) if check.status == SigStatus::Unknown => None, + Err(SignError::InvalidSignatureFormat) => None, + e => Some(e), + }) + .transpose()?; + + if let Some(verification) = verification { + // a key might get imported before next call?. + // realistically this is unlikely, but technically + // it's correct to not cache unknowns here + if verification.status != SigStatus::Unknown { + self.cache + .lock() + .unwrap() + .put(commit_id.clone(), verification.clone()); + } + Ok(verification) + } else { + // now here it's correct to cache unknowns, as we don't + // have a backend that knows how to handle this signature + // + // not sure about how much of an optimization this is + self.cache + .lock() + .unwrap() + .put(commit_id.clone(), Verification::unknown()); + Ok(Verification::unknown()) + } + } +} + +/// The backend for signing and verifying cryptographic signatures. +/// +/// This allows using different signers, such as GPG or SSH, or different +/// versions of them. +pub trait SigningBackend: Debug + Send + Sync { + /// Name of the backend, used in the config and for display. + fn name(&self) -> &str; + + /// Check if the signature can be read and verified by this backend. + /// + /// Should check the signature format, usually just looks at the prefix. + fn can_read(&self, signature: &[u8]) -> bool; + + /// Create a signature for arbitrary data. + /// + /// The `key` parameter is what `jj sign` receives as key argument, or what + /// is configured in the `signing.key` config. + fn sign(&self, data: &[u8], key: Option<&str>) -> SignResult>; + + /// Verify a signature. Should be reflexive with `sign`: + /// ```rust,ignore + /// verify(data, sign(data)?)?.status == SigStatus::Good + /// ``` + fn verify(&self, data: &[u8], signature: &[u8]) -> SignResult; +} + +/// An error type for the signing/verifying operations +#[derive(Debug, Error)] +pub enum SignError { + /// The verification failed because the signature *format* was invalid. + #[error("Invalid signature")] + InvalidSignatureFormat, + /// A generic error from the backend impl. + #[error("Signing error")] + Backend(#[source] Box), +} + +/// A result type for the signing/verifying operations +pub type SignResult = Result; diff --git a/lib/core/src/workspace_store.rs b/lib/core/src/workspace_store.rs new file mode 100644 index 00000000000..a1f2f6bbe79 --- /dev/null +++ b/lib/core/src/workspace_store.rs @@ -0,0 +1,56 @@ +// Copyright 2026 The Jujutsu Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Workspace store for managing workspace metadata. + +use std::fmt::Debug; +use std::path::Path; +use std::path::PathBuf; + +use thiserror::Error; + +use crate::ref_name::WorkspaceName; + +/// Errors that can occur when interacting with a workspace store. +#[derive(Error, Debug)] +pub enum WorkspaceStoreError { + /// An unspecified error occurred. + #[error(transparent)] + Other(#[from] Box), +} + +/// A storage backend for workspace metadata. +pub trait WorkspaceStore: Send + Sync + Debug { + /// Returns the name of this workspace store implementation. + fn name(&self) -> &str; + + /// Adds a workspace with the given name and path to the store. + fn add(&self, workspace_name: &WorkspaceName, path: &Path) -> Result<(), WorkspaceStoreError>; + + /// Forgets the workspaces with the given names. + fn forget(&self, workspace_names: &[&WorkspaceName]) -> Result<(), WorkspaceStoreError>; + + /// Renames a workspace from `old_name` to `new_name`. + fn rename( + &self, + old_name: &WorkspaceName, + new_name: &WorkspaceName, + ) -> Result<(), WorkspaceStoreError>; + + /// Gets the path of the workspace with the given name, if it exists. + fn get_workspace_path( + &self, + workspace_name: &WorkspaceName, + ) -> Result, WorkspaceStoreError>; +} diff --git a/lib/proc-macros/Cargo.toml b/lib/proc-macros/Cargo.toml index 8f2c0003421..29cd7d66ede 100644 --- a/lib/proc-macros/Cargo.toml +++ b/lib/proc-macros/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "jj-lib-proc-macros" -description = "Proc macros for the jj-lib crate" +description = "Proc macros for the jj-lib crate (deprecated use jj-core-proc-macros instead)" version = { workspace = true } edition = { workspace = true } @@ -17,9 +17,6 @@ include = ["/LICENSE", "/src/"] proc-macro = true [dependencies] -proc-macro2 = { workspace = true } -quote = { workspace = true } -syn = { workspace = true } [lints] workspace = true diff --git a/lib/proc-macros/src/lib.rs b/lib/proc-macros/src/lib.rs index 83937aece71..aff5e7133bd 100644 --- a/lib/proc-macros/src/lib.rs +++ b/lib/proc-macros/src/lib.rs @@ -1,38 +1,2 @@ -mod content_hash; - -extern crate proc_macro; - -use quote::quote; -use syn::DeriveInput; -use syn::parse_macro_input; - -/// Derive macro generating an impl of the trait `ContentHash`. -/// -/// Derives the `ContentHash` trait for a struct by calling `ContentHash::hash` -/// on each of the struct members in the order that they're declared. All -/// members of the struct must implement the `ContentHash` trait. -#[proc_macro_derive(ContentHash)] -pub fn derive_content_hash(input: proc_macro::TokenStream) -> proc_macro::TokenStream { - let input = parse_macro_input!(input as DeriveInput); - - // The name of the struct. - let name = &input.ident; - - // Generate an expression to hash each of the fields in the struct. - let hash_impl = content_hash::generate_hash_impl(&input.data); - - // Handle structs and enums with generics. - let generics = content_hash::add_trait_bounds(input.generics); - let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); - - let expanded = quote! { - #[automatically_derived] - impl #impl_generics ::jj_lib::content_hash::ContentHash for #name #ty_generics - #where_clause { - fn hash(&self, state: &mut impl ::jj_lib::content_hash::DigestUpdate) { - #hash_impl - } - } - }; - expanded.into() -} +//! Don't use this crate, use `jj-core-proc-macros` instead since it now +//! contains the `ContentHash` macro. diff --git a/lib/src/backend.rs b/lib/src/backend.rs index 05ccd280c3c..cf4b9299611 100644 --- a/lib/src/backend.rs +++ b/lib/src/backend.rs @@ -25,10 +25,11 @@ use async_trait::async_trait; use chrono::TimeZone as _; use futures::AsyncRead; use futures::stream::BoxStream; +pub use jj_core::backend::ChangeId; +pub use jj_core::backend::CommitId; use thiserror::Error; use crate::content_hash::ContentHash; -use crate::hex_util; use crate::index::Index; use crate::merge::Merge; use crate::object_id::ObjectId as _; @@ -39,16 +40,6 @@ use crate::repo_path::RepoPathComponent; use crate::repo_path::RepoPathComponentBuf; use crate::signing::SignResult; -id_type!( - /// Identifier for a [`Commit`] based on its content. When a commit is - /// rewritten, its `CommitId` changes. - pub CommitId { hex() } -); -id_type!( - /// Stable identifier for a [`Commit`]. Unlike the `CommitId`, the `ChangeId` - /// follows the commit and is not updated when the commit is rewritten. - pub ChangeId { reverse_hex() } -); id_type!( /// Identifier for a tree object. pub TreeId { hex() } @@ -66,19 +57,6 @@ id_type!( pub CopyId { hex() } ); -impl ChangeId { - /// Parses the given "reverse" hex string into a `ChangeId`. - pub fn try_from_reverse_hex(hex: impl AsRef<[u8]>) -> Option { - hex_util::decode_reverse_hex(hex).map(Self) - } - - /// Returns the hex string representation of this ID, which uses `z-k` - /// "digits" instead of `0-9a-f`. - pub fn reverse_hex(&self) -> String { - hex_util::encode_reverse_hex(&self.0) - } -} - impl CopyId { /// Returns a placeholder copy id to be used when we don't have a real copy /// id yet. diff --git a/lib/src/content_hash.rs b/lib/src/content_hash.rs index 0137ac009c9..f6d764a619e 100644 --- a/lib/src/content_hash.rs +++ b/lib/src/content_hash.rs @@ -1,300 +1,7 @@ //! Portable, stable hashing suitable for identifying values -use blake2::Blake2b512; // Re-export DigestUpdate so that the ContentHash proc macro can be used in // external crates without directly depending on the digest crate. pub use digest::Update as DigestUpdate; -use itertools::Itertools as _; -pub use jj_lib_proc_macros::ContentHash; - -/// Portable, stable hashing suitable for identifying values -/// -/// Variable-length sequences should hash a 64-bit little-endian representation -/// of their length, then their elements in order. Unordered containers should -/// order their elements according to their `Ord` implementation. Enums should -/// hash a 32-bit little-endian encoding of the ordinal number of the enum -/// variant, then the variant's fields in lexical order. -/// -/// Structs can implement `ContentHash` by using `#[derive(ContentHash)]`. -pub trait ContentHash { - /// Update the hasher state with this object's content - fn hash(&self, state: &mut impl DigestUpdate); -} - -/// The 512-bit BLAKE2b content hash -pub fn blake2b_hash(x: &(impl ContentHash + ?Sized)) -> digest::Output { - use digest::Digest as _; - let mut hasher = Blake2b512::default(); - x.hash(&mut hasher); - hasher.finalize() -} - -impl ContentHash for () { - fn hash(&self, _: &mut impl DigestUpdate) {} -} - -macro_rules! tuple_impls { - ($( ( $($n:tt $T:ident),+ ) )+) => { - $( - impl<$($T: ContentHash,)+> ContentHash for ($($T,)+) { - fn hash(&self, state: &mut impl DigestUpdate) { - $(self.$n.hash(state);)+ - } - } - )+ - } -} - -tuple_impls! { - (0 T0) - (0 T0, 1 T1) - (0 T0, 1 T1, 2 T2) - (0 T0, 1 T1, 2 T2, 3 T3) -} - -impl ContentHash for bool { - fn hash(&self, state: &mut impl DigestUpdate) { - u8::from(*self).hash(state); - } -} - -impl ContentHash for u8 { - fn hash(&self, state: &mut impl DigestUpdate) { - state.update(&[*self]); - } -} - -impl ContentHash for u32 { - fn hash(&self, state: &mut impl DigestUpdate) { - state.update(&self.to_le_bytes()); - } -} - -impl ContentHash for i32 { - fn hash(&self, state: &mut impl DigestUpdate) { - state.update(&self.to_le_bytes()); - } -} - -impl ContentHash for u64 { - fn hash(&self, state: &mut impl DigestUpdate) { - state.update(&self.to_le_bytes()); - } -} - -impl ContentHash for i64 { - fn hash(&self, state: &mut impl DigestUpdate) { - state.update(&self.to_le_bytes()); - } -} - -// TODO: Specialize for [u8] once specialization exists -impl ContentHash for [T] { - fn hash(&self, state: &mut impl DigestUpdate) { - state.update(&(self.len() as u64).to_le_bytes()); - for x in self { - x.hash(state); - } - } -} - -impl ContentHash for Vec { - fn hash(&self, state: &mut impl DigestUpdate) { - self.as_slice().hash(state); - } -} - -impl ContentHash for str { - fn hash(&self, state: &mut impl DigestUpdate) { - self.as_bytes().hash(state); - } -} - -impl ContentHash for String { - fn hash(&self, state: &mut impl DigestUpdate) { - self.as_str().hash(state); - } -} - -impl ContentHash for Option { - fn hash(&self, state: &mut impl DigestUpdate) { - match self { - None => state.update(&0u32.to_le_bytes()), - Some(x) => { - state.update(&1u32.to_le_bytes()); - x.hash(state); - } - } - } -} - -impl ContentHash for std::collections::HashMap -where - K: ContentHash + Ord, - V: ContentHash, -{ - fn hash(&self, state: &mut impl DigestUpdate) { - state.update(&(self.len() as u64).to_le_bytes()); - let mut kv = self.iter().collect_vec(); - kv.sort_unstable_by_key(|&(k, _)| k); - for (k, v) in kv { - k.hash(state); - v.hash(state); - } - } -} - -impl ContentHash for std::collections::HashSet -where - K: ContentHash + Ord, -{ - fn hash(&self, state: &mut impl DigestUpdate) { - state.update(&(self.len() as u64).to_le_bytes()); - for k in self.iter().sorted() { - k.hash(state); - } - } -} - -impl ContentHash for std::collections::BTreeMap -where - K: ContentHash, - V: ContentHash, -{ - fn hash(&self, state: &mut impl DigestUpdate) { - state.update(&(self.len() as u64).to_le_bytes()); - for (k, v) in self { - k.hash(state); - v.hash(state); - } - } -} - -#[cfg(test)] -mod tests { - use std::collections::BTreeMap; - use std::collections::HashMap; - - use super::*; - use crate::hex_util; - - #[test] - fn test_string_sanity() { - let a = "a".to_string(); - let b = "b".to_string(); - assert_eq!(hash(&a), hash(&a.clone())); - assert_ne!(hash(&a), hash(&b)); - assert_ne!(hash(&"a".to_string()), hash(&"a\0".to_string())); - } - - #[test] - fn test_hash_map_key_value_distinction() { - let a = [("ab".to_string(), "cd".to_string())] - .into_iter() - .collect::>(); - let b = [("a".to_string(), "bcd".to_string())] - .into_iter() - .collect::>(); - - assert_ne!(hash(&a), hash(&b)); - } - - #[test] - fn test_btree_map_key_value_distinction() { - let a = [("ab".to_string(), "cd".to_string())] - .into_iter() - .collect::>(); - let b = [("a".to_string(), "bcd".to_string())] - .into_iter() - .collect::>(); - - assert_ne!(hash(&a), hash(&b)); - } - - #[test] - fn test_tuple_sanity() { - #[derive(ContentHash)] - struct T1(i32); - #[derive(ContentHash)] - struct T2(i32, i32); - #[derive(ContentHash)] - struct T3(i32, i32, i32); - #[derive(ContentHash)] - struct T4(i32, i32, i32, i32); - assert_eq!(hash(&T1(0)), hash(&(0,))); - assert_eq!(hash(&T2(0, 1)), hash(&(0, 1))); - assert_eq!(hash(&T3(0, 1, 2)), hash(&(0, 1, 2))); - assert_eq!(hash(&T4(0, 1, 2, 3)), hash(&(0, 1, 2, 3))); - } - - #[test] - fn test_struct_sanity() { - #[derive(ContentHash)] - struct Foo { - x: i32, - } - assert_ne!(hash(&Foo { x: 42 }), hash(&Foo { x: 12 })); - } - - #[test] - fn test_option_sanity() { - assert_ne!(hash(&Some(42)), hash(&42)); - assert_ne!(hash(&None::), hash(&42i32)); - } - - #[test] - fn test_slice_sanity() { - assert_ne!(hash(&[42i32][..]), hash(&[12i32][..])); - assert_ne!(hash(&([] as [i32; 0])[..]), hash(&[42i32][..])); - assert_ne!(hash(&([] as [i32; 0])[..]), hash(&())); - assert_ne!(hash(&42i32), hash(&[42i32][..])); - } - - #[test] - fn test_consistent_hashing() { - #[derive(ContentHash)] - struct Foo { - x: Vec>, - y: i64, - } - let foo_hash = hex_util::encode_hex(&hash(&Foo { - x: vec![None, Some(42)], - y: 17, - })); - insta::assert_snapshot!( - foo_hash, - @"e33c423b4b774b1353c414e0f9ef108822fde2fd5113fcd53bf7bd9e74e3206690b96af96373f268ed95dd020c7cbe171c7b7a6947fcaf5703ff6c8e208cefd4" - ); - - // Try again with an equivalent generic struct deriving ContentHash. - #[derive(ContentHash)] - struct GenericFoo { - x: X, - y: Y, - } - assert_eq!( - hex_util::encode_hex(&hash(&GenericFoo { - x: vec![None, Some(42)], - y: 17i64 - })), - foo_hash - ); - } - - // Test that the derived version of `ContentHash` matches the that's - // manually implemented for `std::Option`. - #[test] - fn derive_for_enum() { - #[derive(ContentHash)] - enum MyOption { - None, - Some(T), - } - assert_eq!(hash(&Option::::None), hash(&MyOption::::None)); - assert_eq!(hash(&Some(1)), hash(&MyOption::Some(1))); - } - - fn hash(x: &(impl ContentHash + ?Sized)) -> digest::Output { - blake2b_hash(x) - } -} +pub use jj_core::content_hash::ContentHash; +pub use jj_core::content_hash::blake2b_hash; diff --git a/lib/src/dsl_util.rs b/lib/src/dsl_util.rs index 0bad038f3f9..d09102e41e9 100644 --- a/lib/src/dsl_util.rs +++ b/lib/src/dsl_util.rs @@ -14,1074 +14,26 @@ //! Domain-specific language helpers. -use std::ascii; -use std::collections::HashMap; -use std::fmt; -use std::slice; - -use itertools::Itertools as _; -use pest::RuleType; -use pest::iterators::Pair; -use pest::iterators::Pairs; - -/// Manages diagnostic messages emitted during parsing. -/// -/// `T` is usually a parse error type of the language, which contains a message -/// and source span of 'static lifetime. -#[derive(Debug)] -pub struct Diagnostics { - // This might be extended to [{ kind: Warning|Error, message: T }, ..]. - diagnostics: Vec, -} - -impl Diagnostics { - /// Creates new empty diagnostics collector. - pub fn new() -> Self { - Self { - diagnostics: Vec::new(), - } - } - - /// Returns `true` if there are no diagnostic messages. - pub fn is_empty(&self) -> bool { - self.diagnostics.is_empty() - } - - /// Returns the number of diagnostic messages. - pub fn len(&self) -> usize { - self.diagnostics.len() - } - - /// Returns iterator over diagnostic messages. - pub fn iter(&self) -> slice::Iter<'_, T> { - self.diagnostics.iter() - } - - /// Adds a diagnostic message of warning level. - pub fn add_warning(&mut self, diag: T) { - self.diagnostics.push(diag); - } - - /// Moves diagnostic messages of different type (such as fileset warnings - /// emitted within `file()` revset.) - pub fn extend_with(&mut self, diagnostics: Diagnostics, mut f: impl FnMut(U) -> T) { - self.diagnostics - .extend(diagnostics.diagnostics.into_iter().map(&mut f)); - } -} - -impl Default for Diagnostics { - fn default() -> Self { - Self::new() - } -} - -impl<'a, T> IntoIterator for &'a Diagnostics { - type Item = &'a T; - type IntoIter = slice::Iter<'a, T>; - - fn into_iter(self) -> Self::IntoIter { - self.iter() - } -} - -/// AST node without type or name checking. -#[derive(Clone, Debug, Eq, PartialEq)] -pub struct ExpressionNode<'i, T> { - /// Expression item such as identifier, literal, function call, etc. - pub kind: T, - /// Span of the node. - pub span: pest::Span<'i>, -} - -impl<'i, T> ExpressionNode<'i, T> { - /// Wraps the given expression and span. - pub fn new(kind: T, span: pest::Span<'i>) -> Self { - Self { kind, span } - } -} - -/// `:` expression in AST. -#[derive(Clone, Debug, Eq, PartialEq)] -pub struct PatternNode<'i, T> { - /// Pattern name or type (such as `glob`.) - pub name: &'i str, - /// Span of the pattern name. - pub name_span: pest::Span<'i>, - /// Value expression. - pub value: ExpressionNode<'i, T>, -} - -/// Function call in AST. -#[derive(Clone, Debug, Eq, PartialEq)] -pub struct FunctionCallNode<'i, T> { - /// Function name. - pub name: &'i str, - /// Span of the function name. - pub name_span: pest::Span<'i>, - /// List of positional arguments. - pub args: Vec>, - /// List of keyword arguments. - pub keyword_args: Vec>, - /// Span of the arguments list. - pub args_span: pest::Span<'i>, -} - -/// Keyword argument pair in AST. -#[derive(Clone, Debug, Eq, PartialEq)] -pub struct KeywordArgument<'i, T> { - /// Parameter name. - pub name: &'i str, - /// Span of the parameter name. - pub name_span: pest::Span<'i>, - /// Value expression. - pub value: ExpressionNode<'i, T>, -} - -impl<'i, T> FunctionCallNode<'i, T> { - /// Number of arguments assuming named arguments are all unique. - pub fn arity(&self) -> usize { - self.args.len() + self.keyword_args.len() - } - - /// Ensures that no arguments passed. - pub fn expect_no_arguments(&self) -> Result<(), InvalidArguments<'i>> { - let ([], []) = self.expect_arguments()?; - Ok(()) - } - - /// Extracts exactly N required arguments. - pub fn expect_exact_arguments( - &self, - ) -> Result<&[ExpressionNode<'i, T>; N], InvalidArguments<'i>> { - let (args, []) = self.expect_arguments()?; - Ok(args) - } - - /// Extracts N required arguments and remainders. - /// - /// This can be used to get all the positional arguments without requiring - /// any (N = 0): - /// ```ignore - /// let ([], content_nodes) = function.expect_some_arguments()?; - /// ``` - /// Avoid accessing `function.args` directly, as that may allow keyword - /// arguments to be silently ignored. - #[expect(clippy::type_complexity)] - pub fn expect_some_arguments( - &self, - ) -> Result<(&[ExpressionNode<'i, T>; N], &[ExpressionNode<'i, T>]), InvalidArguments<'i>> { - self.ensure_no_keyword_arguments()?; - if self.args.len() >= N { - let (required, rest) = self.args.split_at(N); - Ok((required.try_into().unwrap(), rest)) - } else { - Err(self.invalid_arguments_count(N, None)) - } - } - - /// Extracts N required arguments and M optional arguments. - #[expect(clippy::type_complexity)] - pub fn expect_arguments( - &self, - ) -> Result< - ( - &[ExpressionNode<'i, T>; N], - [Option<&ExpressionNode<'i, T>>; M], - ), - InvalidArguments<'i>, - > { - self.ensure_no_keyword_arguments()?; - let count_range = N..=(N + M); - if count_range.contains(&self.args.len()) { - let (required, rest) = self.args.split_at(N); - let mut optional = rest.iter().map(Some).collect_vec(); - optional.resize(M, None); - Ok(( - required.try_into().unwrap(), - optional.try_into().ok().unwrap(), - )) - } else { - let (min, max) = count_range.into_inner(); - Err(self.invalid_arguments_count(min, Some(max))) - } - } - - /// Extracts N required arguments and M optional arguments. Some of them can - /// be specified as keyword arguments. - /// - /// `names` is a list of parameter names. Unnamed positional arguments - /// should be padded with `""`. - #[expect(clippy::type_complexity)] - pub fn expect_named_arguments( - &self, - names: &[&str], - ) -> Result< - ( - [&ExpressionNode<'i, T>; N], - [Option<&ExpressionNode<'i, T>>; M], - ), - InvalidArguments<'i>, - > { - if self.keyword_args.is_empty() { - let (required, optional) = self.expect_arguments::()?; - Ok((required.each_ref(), optional)) - } else { - let (required, optional) = self.expect_named_arguments_vec(names, N, N + M)?; - Ok(( - required.try_into().ok().unwrap(), - optional.try_into().ok().unwrap(), - )) - } - } - - #[expect(clippy::type_complexity)] - fn expect_named_arguments_vec( - &self, - names: &[&str], - min: usize, - max: usize, - ) -> Result< - ( - Vec<&ExpressionNode<'i, T>>, - Vec>>, - ), - InvalidArguments<'i>, - > { - assert!(names.len() <= max); - - if self.args.len() > max { - return Err(self.invalid_arguments_count(min, Some(max))); - } - let mut extracted = Vec::with_capacity(max); - extracted.extend(self.args.iter().map(Some)); - extracted.resize(max, None); - - for arg in &self.keyword_args { - let name = arg.name; - let span = arg.name_span.start_pos().span(&arg.value.span.end_pos()); - let pos = names.iter().position(|&n| n == name).ok_or_else(|| { - self.invalid_arguments(format!(r#"Unexpected keyword argument "{name}""#), span) - })?; - if extracted[pos].is_some() { - return Err(self.invalid_arguments( - format!(r#"Got multiple values for keyword "{name}""#), - span, - )); - } - extracted[pos] = Some(&arg.value); - } - - let optional = extracted.split_off(min); - let required = extracted.into_iter().flatten().collect_vec(); - if required.len() != min { - return Err(self.invalid_arguments_count(min, Some(max))); - } - Ok((required, optional)) - } - - fn ensure_no_keyword_arguments(&self) -> Result<(), InvalidArguments<'i>> { - if let (Some(first), Some(last)) = (self.keyword_args.first(), self.keyword_args.last()) { - let span = first.name_span.start_pos().span(&last.value.span.end_pos()); - Err(self.invalid_arguments("Unexpected keyword arguments".to_owned(), span)) - } else { - Ok(()) - } - } - - fn invalid_arguments(&self, message: String, span: pest::Span<'i>) -> InvalidArguments<'i> { - InvalidArguments { - name: self.name, - message, - span, - } - } - - fn invalid_arguments_count(&self, min: usize, max: Option) -> InvalidArguments<'i> { - let message = match (min, max) { - (min, Some(max)) if min == max => format!("Expected {min} arguments"), - (min, Some(max)) => format!("Expected {min} to {max} arguments"), - (min, None) => format!("Expected at least {min} arguments"), - }; - self.invalid_arguments(message, self.args_span) - } - - fn invalid_arguments_count_with_arities( - &self, - arities: impl IntoIterator, - ) -> InvalidArguments<'i> { - let message = format!("Expected {} arguments", arities.into_iter().join(", ")); - self.invalid_arguments(message, self.args_span) - } -} - -/// Unexpected number of arguments, or invalid combination of arguments. -/// -/// This error is supposed to be converted to language-specific parse error -/// type, where lifetime `'i` will be eliminated. -#[derive(Clone, Debug)] -pub struct InvalidArguments<'i> { - /// Function name. - pub name: &'i str, - /// Error message. - pub message: String, - /// Span of the bad arguments. - pub span: pest::Span<'i>, -} - -/// Expression item that can be transformed recursively by using `folder: F`. -pub trait FoldableExpression<'i>: Sized { - /// Transforms `self` by applying the `folder` to inner items. - fn fold(self, folder: &mut F, span: pest::Span<'i>) -> Result - where - F: ExpressionFolder<'i, Self> + ?Sized; -} - -/// Visitor-like interface to transform AST nodes recursively. -pub trait ExpressionFolder<'i, T: FoldableExpression<'i>> { - /// Transform error. - type Error; - - /// Transforms the expression `node`. By default, inner items are - /// transformed recursively. - fn fold_expression( - &mut self, - node: ExpressionNode<'i, T>, - ) -> Result, Self::Error> { - let ExpressionNode { kind, span } = node; - let kind = kind.fold(self, span)?; - Ok(ExpressionNode { kind, span }) - } - - /// Transforms identifier. - fn fold_identifier(&mut self, name: &'i str, span: pest::Span<'i>) -> Result; - - /// Transforms pattern. - fn fold_pattern( - &mut self, - pattern: Box>, - span: pest::Span<'i>, - ) -> Result; - - /// Transforms function call. - fn fold_function_call( - &mut self, - function: Box>, - span: pest::Span<'i>, - ) -> Result; -} - -/// Transforms list of `nodes` by using `folder`. -pub fn fold_expression_nodes<'i, F, T>( - folder: &mut F, - nodes: Vec>, -) -> Result>, F::Error> -where - F: ExpressionFolder<'i, T> + ?Sized, - T: FoldableExpression<'i>, -{ - nodes - .into_iter() - .map(|node| folder.fold_expression(node)) - .try_collect() -} - -/// Transforms pattern value by using `folder`. -pub fn fold_pattern_value<'i, F, T>( - folder: &mut F, - pattern: PatternNode<'i, T>, -) -> Result, F::Error> -where - F: ExpressionFolder<'i, T> + ?Sized, - T: FoldableExpression<'i>, -{ - Ok(PatternNode { - name: pattern.name, - name_span: pattern.name_span, - value: folder.fold_expression(pattern.value)?, - }) -} - -/// Transforms function call arguments by using `folder`. -pub fn fold_function_call_args<'i, F, T>( - folder: &mut F, - function: FunctionCallNode<'i, T>, -) -> Result, F::Error> -where - F: ExpressionFolder<'i, T> + ?Sized, - T: FoldableExpression<'i>, -{ - Ok(FunctionCallNode { - name: function.name, - name_span: function.name_span, - args: fold_expression_nodes(folder, function.args)?, - keyword_args: function - .keyword_args - .into_iter() - .map(|arg| { - Ok(KeywordArgument { - name: arg.name, - name_span: arg.name_span, - value: folder.fold_expression(arg.value)?, - }) - }) - .try_collect()?, - args_span: function.args_span, - }) -} - -/// Helper to parse string literal. -#[derive(Debug)] -pub struct StringLiteralParser { - /// String content part. - pub content_rule: R, - /// Escape sequence part including backslash character. - pub escape_rule: R, -} - -impl StringLiteralParser { - /// Parses the given string literal `pairs` into string. - pub fn parse(&self, pairs: Pairs) -> String { - let mut result = String::new(); - for part in pairs { - if part.as_rule() == self.content_rule { - result.push_str(part.as_str()); - } else if part.as_rule() == self.escape_rule { - match &part.as_str()[1..] { - "\"" => result.push('"'), - "\\" => result.push('\\'), - "t" => result.push('\t'), - "r" => result.push('\r'), - "n" => result.push('\n'), - "0" => result.push('\0'), - "e" => result.push('\x1b'), - hex if hex.starts_with('x') => { - result.push(char::from( - u8::from_str_radix(&hex[1..], 16).expect("hex characters"), - )); - } - char => panic!("invalid escape: \\{char:?}"), - } - } else { - panic!("unexpected part of string: {part:?}"); - } - } - result - } -} - -/// Escape special characters in the input -pub fn escape_string(unescaped: &str) -> String { - let mut escaped = String::with_capacity(unescaped.len()); - for c in unescaped.chars() { - match c { - '"' => escaped.push_str(r#"\""#), - '\\' => escaped.push_str(r#"\\"#), - '\t' => escaped.push_str(r#"\t"#), - '\r' => escaped.push_str(r#"\r"#), - '\n' => escaped.push_str(r#"\n"#), - '\0' => escaped.push_str(r#"\0"#), - c if c.is_ascii_control() => { - for b in ascii::escape_default(c as u8) { - escaped.push(b as char); - } - } - c => escaped.push(c), - } - } - escaped -} - -/// Helper to parse function call. -#[derive(Debug)] -pub struct FunctionCallParser { - /// Function name. - pub function_name_rule: R, - /// List of positional and keyword arguments. - pub function_arguments_rule: R, - /// Pair of parameter name and value. - pub keyword_argument_rule: R, - /// Parameter name. - pub argument_name_rule: R, - /// Value expression. - pub argument_value_rule: R, -} - -impl FunctionCallParser { - /// Parses the given `pair` as function call. - pub fn parse<'i, T, E: From>>( - &self, - pair: Pair<'i, R>, - // parse_name can be defined for any Pair<'_, R>, but parse_value should - // be allowed to construct T by capturing Pair<'i, R>. - parse_name: impl Fn(Pair<'i, R>) -> Result<&'i str, E>, - parse_value: impl Fn(Pair<'i, R>) -> Result, E>, - ) -> Result, E> { - let [name_pair, args_pair] = pair.into_inner().collect_array().unwrap(); - assert_eq!(name_pair.as_rule(), self.function_name_rule); - assert_eq!(args_pair.as_rule(), self.function_arguments_rule); - let name_span = name_pair.as_span(); - let args_span = args_pair.as_span(); - let function_name = parse_name(name_pair)?; - let mut args = Vec::new(); - let mut keyword_args = Vec::new(); - for pair in args_pair.into_inner() { - let span = pair.as_span(); - if pair.as_rule() == self.argument_value_rule { - if !keyword_args.is_empty() { - return Err(InvalidArguments { - name: function_name, - message: "Positional argument follows keyword argument".to_owned(), - span, - } - .into()); - } - args.push(parse_value(pair)?); - } else if pair.as_rule() == self.keyword_argument_rule { - let [name_pair, value_pair] = pair.into_inner().collect_array().unwrap(); - assert_eq!(name_pair.as_rule(), self.argument_name_rule); - assert_eq!(value_pair.as_rule(), self.argument_value_rule); - let name_span = name_pair.as_span(); - let arg = KeywordArgument { - name: parse_name(name_pair)?, - name_span, - value: parse_value(value_pair)?, - }; - keyword_args.push(arg); - } else { - panic!("unexpected argument rule {pair:?}"); - } - } - Ok(FunctionCallNode { - name: function_name, - name_span, - args, - keyword_args, - args_span, - }) - } -} - -/// A function alias containing `(params, definition, description)`. -type FunctionAlias = (Vec, V, Option); - -/// Map of symbol, pattern, and function aliases. -#[derive(Clone, Debug, Default)] -pub struct AliasesMap { - symbol_aliases: HashMap)>, - // name: (param, defn) - pattern_aliases: HashMap)>, - // name: [(params, defn)] (sorted by arity) - function_aliases: HashMap>>, - // Parser type P helps prevent misuse of AliasesMap of different language. - parser: P, -} - -impl AliasesMap { - /// Creates an empty aliases map with default-constructed parser. - pub fn new() -> Self - where - P: Default, - { - Self { - symbol_aliases: Default::default(), - pattern_aliases: Default::default(), - function_aliases: Default::default(), - parser: Default::default(), - } - } - - /// Adds new substitution rule `decl = defn`. - /// - /// Returns error if `decl` is invalid. The `defn` part isn't checked. A bad - /// `defn` will be reported when the alias is substituted. - pub fn insert( - &mut self, - decl: impl AsRef, - defn: impl Into, - doc: Option, - ) -> Result<(), P::Error> - where - P: AliasDeclarationParser, - { - match self.parser.parse_declaration(decl.as_ref())? { - AliasDeclaration::Symbol(name) => { - self.symbol_aliases.insert(name, (defn.into(), doc)); - } - AliasDeclaration::Pattern(name, param) => { - self.pattern_aliases.insert(name, (param, defn.into(), doc)); - } - AliasDeclaration::Function(name, params) => { - let overloads = self.function_aliases.entry(name).or_default(); - match overloads.binary_search_by_key(¶ms.len(), |(params, _, _)| params.len()) { - Ok(i) => overloads[i] = (params, defn.into(), doc), - Err(i) => overloads.insert(i, (params, defn.into(), doc)), - } - } - } - Ok(()) - } - - /// Iterates symbol names in arbitrary order. - pub fn symbol_names(&self) -> impl Iterator { - self.symbol_aliases.keys().map(|n| n.as_ref()) - } - - /// Iterates pattern names in arbitrary order. - pub fn pattern_names(&self) -> impl Iterator { - self.pattern_aliases.keys().map(|n| n.as_ref()) - } - - /// Iterates function names in arbitrary order. - pub fn function_names(&self) -> impl Iterator { - self.function_aliases.keys().map(|n| n.as_ref()) - } - - /// Looks up symbol alias by name. Returns identifier, definition text, and - /// optional description. - pub fn get_symbol(&self, name: &str) -> Option<(AliasId<'_>, &V, Option<&str>)> { - self.symbol_aliases - .get_key_value(name) - .map(|(name, (defn, doc))| (AliasId::Symbol(name), defn, doc.as_deref())) - } - - /// Looks up pattern alias by name. Returns identifier, parameter name, - /// definition text, and optional description. - pub fn get_pattern(&self, name: &str) -> Option<(AliasId<'_>, &str, &V, Option<&str>)> { - self.pattern_aliases - .get_key_value(name) - .map(|(name, (param, defn, doc))| { - ( - AliasId::Pattern(name, param), - param.as_ref(), - defn, - doc.as_deref(), - ) - }) - } - - /// Looks up function alias by name and arity. Returns identifier, list of - /// parameter names, definition text, and optional description. - pub fn get_function( - &self, - name: &str, - arity: usize, - ) -> Option<(AliasId<'_>, &[String], &V, Option<&str>)> { - let overloads = self.get_function_overloads(name)?; - overloads.find_by_arity(arity) - } - - /// Looks up function aliases by name. - fn get_function_overloads(&self, name: &str) -> Option> { - let (name, overloads) = self.function_aliases.get_key_value(name)?; - Some(AliasFunctionOverloads { name, overloads }) - } -} - -#[derive(Clone, Debug)] -struct AliasFunctionOverloads<'a, V> { - name: &'a String, - overloads: &'a Vec<(Vec, V, Option)>, -} - -impl<'a, V> AliasFunctionOverloads<'a, V> { - fn arities(&self) -> impl DoubleEndedIterator + ExactSizeIterator { - self.overloads.iter().map(|(params, _, _)| params.len()) - } - - fn min_arity(&self) -> usize { - self.arities().next().unwrap() - } - - fn max_arity(&self) -> usize { - self.arities().next_back().unwrap() - } - - fn find_by_arity( - &self, - arity: usize, - ) -> Option<(AliasId<'a>, &'a [String], &'a V, Option<&'a str>)> { - let index = self - .overloads - .binary_search_by_key(&arity, |(params, _, _)| params.len()) - .ok()?; - let (params, defn, doc) = &self.overloads[index]; - // Exact parameter names aren't needed to identify a function, but they - // provide a better error indication. (e.g. "foo(x, y)" is easier to - // follow than "foo/2".) - Some(( - AliasId::Function(self.name, params), - params, - defn, - doc.as_deref(), - )) - } -} - -/// Borrowed reference to identify alias expression. -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub enum AliasId<'a> { - /// Symbol name. - Symbol(&'a str), - /// Pattern name and parameter name. - Pattern(&'a str, &'a str), - /// Function name and parameter names. - Function(&'a str, &'a [String]), - /// Function parameter name. - Parameter(&'a str), -} - -impl fmt::Display for AliasId<'_> { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::Symbol(name) => write!(f, "{name}"), - Self::Pattern(name, param) => write!(f, "{name}:{param}"), - Self::Function(name, params) => { - write!(f, "{name}({params})", params = params.join(", ")) - } - Self::Parameter(name) => write!(f, "{name}"), - } - } -} - -/// Parsed declaration part of alias rule. -#[derive(Clone, Debug)] -pub enum AliasDeclaration { - /// Symbol name. - Symbol(String), - /// Pattern name and parameter. - Pattern(String, String), - /// Function name and parameters. - Function(String, Vec), -} - -// AliasDeclarationParser and AliasDefinitionParser can be merged into a single -// trait, but it's unclear whether doing that would simplify the abstraction. - -/// Parser for symbol and function alias declaration. -pub trait AliasDeclarationParser { - /// Parse error type. - type Error; - - /// Parses symbol or function name and parameters. - fn parse_declaration(&self, source: &str) -> Result; -} - -/// Parser for symbol and function alias definition. -pub trait AliasDefinitionParser { - /// Expression item type. - type Output<'i>; - /// Parse error type. - type Error; - - /// Parses alias body. - fn parse_definition<'i>( - &self, - source: &'i str, - ) -> Result>, Self::Error>; -} - -/// Expression item that supports alias substitution. -pub trait AliasExpandableExpression<'i>: FoldableExpression<'i> { - /// Wraps identifier. - fn identifier(name: &'i str) -> Self; - /// Wraps pattern. - fn pattern(pattern: Box>) -> Self; - /// Wraps function call. - fn function_call(function: Box>) -> Self; - /// Wraps substituted expression. - fn alias_expanded(id: AliasId<'i>, subst: Box>) -> Self; -} - -/// Error that may occur during alias substitution. -pub trait AliasExpandError: Sized { - /// Unexpected number of arguments, or invalid combination of arguments. - fn invalid_arguments(err: InvalidArguments<'_>) -> Self; - /// Recursion detected during alias substitution. - fn recursive_expansion(id: AliasId<'_>, span: pest::Span<'_>) -> Self; - /// Attaches alias trace to the current error. - fn within_alias_expansion(self, id: AliasId<'_>, span: pest::Span<'_>) -> Self; -} - -/// Expands aliases recursively in tree of `T`. -#[derive(Debug)] -struct AliasExpander<'i, 'a, T, P> { - /// Alias symbols and functions that are globally available. - aliases_map: &'i AliasesMap, - /// Local variables set in the outermost scope. - locals: &'a HashMap<&'i str, ExpressionNode<'i, T>>, - /// Stack of aliases and local parameters currently expanding. - states: Vec>, -} - -#[derive(Debug)] -struct AliasExpandingState<'i, T> { - id: AliasId<'i>, - locals: HashMap<&'i str, ExpressionNode<'i, T>>, -} - -impl<'i, T, P, E> AliasExpander<'i, '_, T, P> -where - T: AliasExpandableExpression<'i> + Clone, - P: AliasDefinitionParser = T, Error = E>, - E: AliasExpandError, -{ - /// Local variables available to the current scope. - fn current_locals(&self) -> &HashMap<&'i str, ExpressionNode<'i, T>> { - self.states.last().map_or(self.locals, |s| &s.locals) - } - - fn expand_defn( - &mut self, - id: AliasId<'i>, - defn: &'i str, - locals: HashMap<&'i str, ExpressionNode<'i, T>>, - span: pest::Span<'i>, - ) -> Result { - // The stack should be short, so let's simply do linear search. - if self.states.iter().any(|s| s.id == id) { - return Err(E::recursive_expansion(id, span)); - } - self.states.push(AliasExpandingState { id, locals }); - // Parsed defn could be cached if needed. - let result = self - .aliases_map - .parser - .parse_definition(defn) - .and_then(|node| self.fold_expression(node)) - .map(|node| T::alias_expanded(id, Box::new(node))) - .map_err(|e| e.within_alias_expansion(id, span)); - self.states.pop(); - result - } -} - -impl<'i, T, P, E> ExpressionFolder<'i, T> for AliasExpander<'i, '_, T, P> -where - T: AliasExpandableExpression<'i> + Clone, - P: AliasDefinitionParser = T, Error = E>, - E: AliasExpandError, -{ - type Error = E; - - fn fold_identifier(&mut self, name: &'i str, span: pest::Span<'i>) -> Result { - if let Some(subst) = self.current_locals().get(name) { - let id = AliasId::Parameter(name); - Ok(T::alias_expanded(id, Box::new(subst.clone()))) - } else if let Some((id, defn, _doc)) = self.aliases_map.get_symbol(name) { - let locals = HashMap::new(); // Don't spill out the current scope - self.expand_defn(id, defn, locals, span) - } else { - Ok(T::identifier(name)) - } - } - - fn fold_pattern( - &mut self, - pattern: Box>, - span: pest::Span<'i>, - ) -> Result { - if let Some((id, param, defn, _doc)) = self.aliases_map.get_pattern(pattern.name) { - // Resolve argument in the current scope, and pass it in to the - // alias expansion scope. - let arg = self.fold_expression(pattern.value)?; - let locals = HashMap::from([(param, arg)]); - self.expand_defn(id, defn, locals, span) - } else { - let pattern = Box::new(fold_pattern_value(self, *pattern)?); - Ok(T::pattern(pattern)) - } - } - - fn fold_function_call( - &mut self, - function: Box>, - span: pest::Span<'i>, - ) -> Result { - // For better error indication, builtin functions are shadowed by name, - // not by (name, arity). - if let Some(overloads) = self.aliases_map.get_function_overloads(function.name) { - // TODO: add support for keyword arguments - function - .ensure_no_keyword_arguments() - .map_err(E::invalid_arguments)?; - let Some((id, params, defn, _doc)) = overloads.find_by_arity(function.arity()) else { - let min = overloads.min_arity(); - let max = overloads.max_arity(); - let err = if max - min + 1 == overloads.arities().len() { - function.invalid_arguments_count(min, Some(max)) - } else { - function.invalid_arguments_count_with_arities(overloads.arities()) - }; - return Err(E::invalid_arguments(err)); - }; - // Resolve arguments in the current scope, and pass them in to the alias - // expansion scope. - let args = fold_expression_nodes(self, function.args)?; - let locals = params.iter().map(|s| s.as_str()).zip(args).collect(); - self.expand_defn(id, defn, locals, span) - } else { - let function = Box::new(fold_function_call_args(self, *function)?); - Ok(T::function_call(function)) - } - } -} - -/// Expands aliases recursively. -pub fn expand_aliases<'i, T, P>( - node: ExpressionNode<'i, T>, - aliases_map: &'i AliasesMap, -) -> Result, P::Error> -where - T: AliasExpandableExpression<'i> + Clone, - P: AliasDefinitionParser = T>, - P::Error: AliasExpandError, -{ - expand_aliases_with_locals(node, aliases_map, &HashMap::new()) -} - -/// Expands aliases recursively with the outermost local variables. -/// -/// Local variables are similar to alias symbols, but are scoped. Alias symbols -/// are globally accessible from alias expressions, but local variables aren't. -pub fn expand_aliases_with_locals<'i, T, P>( - node: ExpressionNode<'i, T>, - aliases_map: &'i AliasesMap, - locals: &HashMap<&'i str, ExpressionNode<'i, T>>, -) -> Result, P::Error> -where - T: AliasExpandableExpression<'i> + Clone, - P: AliasDefinitionParser = T>, - P::Error: AliasExpandError, -{ - let mut expander = AliasExpander { - aliases_map, - locals, - states: Vec::new(), - }; - expander.fold_expression(node) -} - -/// Collects similar names from the `candidates` list. -pub fn collect_similar(name: &str, candidates: I) -> Vec -where - I: IntoIterator, - I::Item: AsRef, -{ - candidates - .into_iter() - .filter(|cand| { - // The parameter is borrowed from clap f5540d26 - strsim::jaro(name, cand.as_ref()) > 0.7 - }) - .map(|s| s.as_ref().to_owned()) - .sorted_unstable() - .collect() -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_expect_arguments() { - fn empty_span() -> pest::Span<'static> { - pest::Span::new("", 0, 0).unwrap() - } - - fn function( - name: &'static str, - args: impl Into>>, - keyword_args: impl Into>>, - ) -> FunctionCallNode<'static, u32> { - FunctionCallNode { - name, - name_span: empty_span(), - args: args.into(), - keyword_args: keyword_args.into(), - args_span: empty_span(), - } - } - - fn value(v: u32) -> ExpressionNode<'static, u32> { - ExpressionNode::new(v, empty_span()) - } - - fn keyword(name: &'static str, v: u32) -> KeywordArgument<'static, u32> { - KeywordArgument { - name, - name_span: empty_span(), - value: value(v), - } - } - - let f = function("foo", [], []); - assert!(f.expect_no_arguments().is_ok()); - assert!(f.expect_some_arguments::<0>().is_ok()); - assert!(f.expect_arguments::<0, 0>().is_ok()); - assert!(f.expect_named_arguments::<0, 0>(&[]).is_ok()); - - let f = function("foo", [value(0)], []); - assert!(f.expect_no_arguments().is_err()); - assert_eq!( - f.expect_some_arguments::<0>().unwrap(), - (&[], [value(0)].as_slice()) - ); - assert_eq!( - f.expect_some_arguments::<1>().unwrap(), - (&[value(0)], [].as_slice()) - ); - assert!(f.expect_arguments::<0, 0>().is_err()); - assert_eq!( - f.expect_arguments::<0, 1>().unwrap(), - (&[], [Some(&value(0))]) - ); - assert_eq!(f.expect_arguments::<1, 1>().unwrap(), (&[value(0)], [None])); - assert!(f.expect_named_arguments::<0, 0>(&[]).is_err()); - assert_eq!( - f.expect_named_arguments::<0, 1>(&["a"]).unwrap(), - ([], [Some(&value(0))]) - ); - assert_eq!( - f.expect_named_arguments::<1, 0>(&["a"]).unwrap(), - ([&value(0)], []) - ); - - let f = function("foo", [], [keyword("a", 0)]); - assert!(f.expect_no_arguments().is_err()); - assert!(f.expect_some_arguments::<1>().is_err()); - assert!(f.expect_arguments::<0, 1>().is_err()); - assert!(f.expect_arguments::<1, 0>().is_err()); - assert!(f.expect_named_arguments::<0, 0>(&[]).is_err()); - assert!(f.expect_named_arguments::<0, 1>(&[]).is_err()); - assert!(f.expect_named_arguments::<1, 0>(&[]).is_err()); - assert_eq!( - f.expect_named_arguments::<1, 0>(&["a"]).unwrap(), - ([&value(0)], []) - ); - assert_eq!( - f.expect_named_arguments::<1, 1>(&["a", "b"]).unwrap(), - ([&value(0)], [None]) - ); - assert!(f.expect_named_arguments::<1, 1>(&["b", "a"]).is_err()); - - let f = function("foo", [value(0)], [keyword("a", 1), keyword("b", 2)]); - assert!(f.expect_named_arguments::<0, 0>(&[]).is_err()); - assert!(f.expect_named_arguments::<1, 1>(&["a", "b"]).is_err()); - assert_eq!( - f.expect_named_arguments::<1, 2>(&["c", "a", "b"]).unwrap(), - ([&value(0)], [Some(&value(1)), Some(&value(2))]) - ); - assert_eq!( - f.expect_named_arguments::<2, 1>(&["c", "b", "a"]).unwrap(), - ([&value(0), &value(2)], [Some(&value(1))]) - ); - assert_eq!( - f.expect_named_arguments::<0, 3>(&["c", "b", "a"]).unwrap(), - ([], [Some(&value(0)), Some(&value(2)), Some(&value(1))]) - ); - - let f = function("foo", [], [keyword("a", 0), keyword("a", 1)]); - assert!(f.expect_named_arguments::<1, 1>(&["", "a"]).is_err()); - } -} +pub use jj_core::dsl_util::AliasDeclaration; +pub use jj_core::dsl_util::AliasDeclarationParser; +pub use jj_core::dsl_util::AliasDefinitionParser; +pub use jj_core::dsl_util::AliasExpandError; +pub use jj_core::dsl_util::AliasExpandableExpression; +pub use jj_core::dsl_util::AliasId; +pub use jj_core::dsl_util::AliasesMap; +pub use jj_core::dsl_util::Diagnostics; +pub use jj_core::dsl_util::ExpressionFolder; +pub use jj_core::dsl_util::ExpressionNode; +pub use jj_core::dsl_util::FoldableExpression; +pub use jj_core::dsl_util::FunctionCallNode; +pub use jj_core::dsl_util::FunctionCallParser; +pub use jj_core::dsl_util::InvalidArguments; +pub use jj_core::dsl_util::KeywordArgument; +pub use jj_core::dsl_util::PatternNode; +pub use jj_core::dsl_util::StringLiteralParser; +pub use jj_core::dsl_util::collect_similar; +pub use jj_core::dsl_util::escape_string; +pub use jj_core::dsl_util::expand_aliases; +pub use jj_core::dsl_util::expand_aliases_with_locals; +pub use jj_core::dsl_util::fold_expression_nodes; +pub use jj_core::dsl_util::fold_function_call_args; diff --git a/lib/src/fileset_parser.rs b/lib/src/fileset_parser.rs index d308df1f398..a1ba1f3784c 100644 --- a/lib/src/fileset_parser.rs +++ b/lib/src/fileset_parser.rs @@ -14,1351 +14,25 @@ //! Parser for the fileset language. -use std::error; -use std::sync::LazyLock; - -use itertools::Itertools as _; -use pest::Parser as _; -use pest::iterators::Pair; -use pest::pratt_parser::Assoc; -use pest::pratt_parser::Op; -use pest::pratt_parser::PrattParser; -use pest_derive::Parser; -use thiserror::Error; - -use crate::dsl_util; -use crate::dsl_util::AliasDeclaration; -use crate::dsl_util::AliasDeclarationParser; -use crate::dsl_util::AliasDefinitionParser; -use crate::dsl_util::AliasExpandError; -use crate::dsl_util::AliasExpandableExpression; -use crate::dsl_util::AliasId; -use crate::dsl_util::AliasesMap; -use crate::dsl_util::Diagnostics; -use crate::dsl_util::ExpressionFolder; -use crate::dsl_util::FoldableExpression; -use crate::dsl_util::InvalidArguments; -use crate::dsl_util::StringLiteralParser; - -#[derive(Parser)] -#[grammar = "fileset.pest"] -struct FilesetParser; - -const STRING_LITERAL_PARSER: StringLiteralParser = StringLiteralParser { - content_rule: Rule::string_content, - escape_rule: Rule::string_escape, -}; - -impl Rule { - fn to_symbol(self) -> Option<&'static str> { - match self { - Self::EOI => None, - Self::whitespace => None, - Self::identifier => None, - Self::strict_identifier_part => None, - Self::strict_identifier => None, - Self::bare_string => None, - Self::string_escape => None, - Self::string_content_char => None, - Self::string_content => None, - Self::string_literal => None, - Self::raw_string_content => None, - Self::raw_string_literal => None, - Self::pattern_kind_op => Some(":"), - Self::negate_op => Some("~"), - Self::union_op => Some("|"), - Self::intersection_op => Some("&"), - Self::difference_op => Some("~"), - Self::prefix_ops => None, - Self::infix_ops => None, - Self::function => None, - Self::function_name => None, - Self::function_arguments => None, - Self::formal_parameters => None, - Self::pattern => None, - Self::bare_string_pattern => None, - Self::primary => None, - Self::expression => None, - Self::program => None, - Self::program_or_bare_string => None, - Self::function_alias_declaration => None, - Self::pattern_alias_declaration => None, - Self::alias_declaration => None, - } - } -} - -/// Manages diagnostic messages emitted during fileset parsing and name -/// resolution. -pub type FilesetDiagnostics = Diagnostics; - -/// Result of fileset parsing and name resolution. -pub type FilesetParseResult = Result; - -/// Error occurred during fileset parsing and name resolution. -#[derive(Debug, Error)] -#[error("{pest_error}")] -pub struct FilesetParseError { - kind: FilesetParseErrorKind, - pest_error: Box>, - source: Option>, -} - -/// Categories of fileset parsing and name resolution error. -#[expect(missing_docs)] -#[derive(Clone, Debug, Eq, Error, PartialEq)] -pub enum FilesetParseErrorKind { - #[error("Syntax error")] - SyntaxError, - #[error("Function `{name}` doesn't exist")] - NoSuchFunction { - name: String, - candidates: Vec, - }, - #[error("Function `{name}`: {message}")] - InvalidArguments { name: String, message: String }, - #[error("Redefinition of function parameter")] - RedefinedFunctionParameter, - #[error("{0}")] - Expression(String), - #[error("In alias `{0}`")] - InAliasExpansion(String), - #[error("In function parameter `{0}`")] - InParameterExpansion(String), - #[error("Alias `{0}` expanded recursively")] - RecursiveAlias(String), -} - -impl FilesetParseError { - pub(super) fn new(kind: FilesetParseErrorKind, span: pest::Span<'_>) -> Self { - let message = kind.to_string(); - let pest_error = Box::new(pest::error::Error::new_from_span( - pest::error::ErrorVariant::CustomError { message }, - span, - )); - Self { - kind, - pest_error, - source: None, - } - } - - pub(super) fn with_source( - mut self, - source: impl Into>, - ) -> Self { - self.source = Some(source.into()); - self - } - - /// Some other expression error. - pub(super) fn expression(message: impl Into, span: pest::Span<'_>) -> Self { - Self::new(FilesetParseErrorKind::Expression(message.into()), span) - } - - /// Category of the underlying error. - pub fn kind(&self) -> &FilesetParseErrorKind { - &self.kind - } -} - -impl AliasExpandError for FilesetParseError { - fn invalid_arguments(err: InvalidArguments<'_>) -> Self { - err.into() - } - - fn recursive_expansion(id: AliasId<'_>, span: pest::Span<'_>) -> Self { - Self::new(FilesetParseErrorKind::RecursiveAlias(id.to_string()), span) - } - - fn within_alias_expansion(self, id: AliasId<'_>, span: pest::Span<'_>) -> Self { - let kind = match id { - AliasId::Symbol(_) | AliasId::Pattern(..) | AliasId::Function(..) => { - FilesetParseErrorKind::InAliasExpansion(id.to_string()) - } - AliasId::Parameter(_) => FilesetParseErrorKind::InParameterExpansion(id.to_string()), - }; - Self::new(kind, span).with_source(self) - } -} - -impl From> for FilesetParseError { - fn from(err: pest::error::Error) -> Self { - Self { - kind: FilesetParseErrorKind::SyntaxError, - pest_error: Box::new(rename_rules_in_pest_error(err)), - source: None, - } - } -} - -impl From> for FilesetParseError { - fn from(err: InvalidArguments<'_>) -> Self { - let kind = FilesetParseErrorKind::InvalidArguments { - name: err.name.to_owned(), - message: err.message, - }; - Self::new(kind, err.span) - } -} - -fn rename_rules_in_pest_error(err: pest::error::Error) -> pest::error::Error { - err.renamed_rules(|rule| { - rule.to_symbol() - .map(|sym| format!("`{sym}`")) - .unwrap_or_else(|| format!("<{rule:?}>")) - }) -} - -#[derive(Clone, Debug, Eq, PartialEq)] -pub enum ExpressionKind<'i> { - Identifier(&'i str), - String(String), - /// `:` where `` is usually `Identifier` or `String`. - Pattern(Box>), - Unary(UnaryOp, Box>), - Binary(BinaryOp, Box>, Box>), - /// `x | y | ..` - UnionAll(Vec>), - FunctionCall(Box>), - /// Identity node to preserve the span in the source text. - AliasExpanded(AliasId<'i>, Box>), -} - -impl<'i> FoldableExpression<'i> for ExpressionKind<'i> { - fn fold(self, folder: &mut F, span: pest::Span<'i>) -> Result - where - F: ExpressionFolder<'i, Self> + ?Sized, - { - match self { - Self::Identifier(name) => folder.fold_identifier(name, span), - Self::String(_) => Ok(self), - Self::Pattern(pattern) => folder.fold_pattern(pattern, span), - Self::Unary(op, arg) => { - let arg = Box::new(folder.fold_expression(*arg)?); - Ok(Self::Unary(op, arg)) - } - Self::Binary(op, lhs, rhs) => { - let lhs = Box::new(folder.fold_expression(*lhs)?); - let rhs = Box::new(folder.fold_expression(*rhs)?); - Ok(Self::Binary(op, lhs, rhs)) - } - Self::UnionAll(nodes) => { - let nodes = dsl_util::fold_expression_nodes(folder, nodes)?; - Ok(Self::UnionAll(nodes)) - } - Self::FunctionCall(function) => folder.fold_function_call(function, span), - Self::AliasExpanded(id, subst) => { - let subst = Box::new(folder.fold_expression(*subst)?); - Ok(Self::AliasExpanded(id, subst)) - } - } - } -} - -impl<'i> AliasExpandableExpression<'i> for ExpressionKind<'i> { - fn identifier(name: &'i str) -> Self { - Self::Identifier(name) - } - - fn pattern(pattern: Box>) -> Self { - Self::Pattern(pattern) - } - - fn function_call(function: Box>) -> Self { - Self::FunctionCall(function) - } - - fn alias_expanded(id: AliasId<'i>, subst: Box>) -> Self { - Self::AliasExpanded(id, subst) - } -} - -#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] -pub enum UnaryOp { - /// `~` - Negate, -} - -#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] -pub enum BinaryOp { - /// `&` - Intersection, - /// `~` - Difference, -} - -pub type ExpressionNode<'i> = dsl_util::ExpressionNode<'i, ExpressionKind<'i>>; -pub type FunctionCallNode<'i> = dsl_util::FunctionCallNode<'i, ExpressionKind<'i>>; -pub type PatternNode<'i> = dsl_util::PatternNode<'i, ExpressionKind<'i>>; - -fn union_nodes<'i>(lhs: ExpressionNode<'i>, rhs: ExpressionNode<'i>) -> ExpressionNode<'i> { - let span = lhs.span.start_pos().span(&rhs.span.end_pos()); - let expr = match lhs.kind { - // Flatten "x | y | z" to save recursion stack. Machine-generated query - // might have long chain of unions. - ExpressionKind::UnionAll(mut nodes) => { - nodes.push(rhs); - ExpressionKind::UnionAll(nodes) - } - _ => ExpressionKind::UnionAll(vec![lhs, rhs]), - }; - ExpressionNode::new(expr, span) -} - -fn parse_function_call_node(pair: Pair) -> FilesetParseResult { - assert_eq!(pair.as_rule(), Rule::function); - let [name_pair, args_pair] = pair.into_inner().collect_array().unwrap(); - assert_eq!(name_pair.as_rule(), Rule::function_name); - assert_eq!(args_pair.as_rule(), Rule::function_arguments); - let name_span = name_pair.as_span(); - let args_span = args_pair.as_span(); - let name = name_pair.as_str(); - let args = args_pair - .into_inner() - .map(parse_expression_node) - .try_collect()?; - Ok(FunctionCallNode { - name, - name_span, - args, - keyword_args: vec![], // unsupported - args_span, - }) -} - -fn parse_as_string_literal(pair: Pair) -> String { - match pair.as_rule() { - Rule::identifier => pair.as_str().to_owned(), - Rule::string_literal => STRING_LITERAL_PARSER.parse(pair.into_inner()), - Rule::raw_string_literal => { - let [content] = pair.into_inner().collect_array().unwrap(); - assert_eq!(content.as_rule(), Rule::raw_string_content); - content.as_str().to_owned() - } - r => panic!("unexpected string literal rule: {r:?}"), - } -} - -fn parse_primary_node(pair: Pair) -> FilesetParseResult { - assert_eq!(pair.as_rule(), Rule::primary); - let span = pair.as_span(); - let first = pair.into_inner().next().unwrap(); - let expr = match first.as_rule() { - // Ignore inner span to preserve parenthesized expression as such. - Rule::expression => parse_expression_node(first)?.kind, - Rule::function => { - let function = Box::new(parse_function_call_node(first)?); - ExpressionKind::FunctionCall(function) - } - Rule::pattern => { - let [lhs, op, rhs] = first.into_inner().collect_array().unwrap(); - assert_eq!(lhs.as_rule(), Rule::strict_identifier); - assert_eq!(op.as_rule(), Rule::pattern_kind_op); - let pattern = Box::new(PatternNode { - name: lhs.as_str(), - name_span: lhs.as_span(), - value: parse_primary_node(rhs)?, - }); - ExpressionKind::Pattern(pattern) - } - Rule::identifier => ExpressionKind::Identifier(first.as_str()), - Rule::string_literal | Rule::raw_string_literal => { - ExpressionKind::String(parse_as_string_literal(first)) - } - r => panic!("unexpected primary rule: {r:?}"), - }; - Ok(ExpressionNode::new(expr, span)) -} - -fn parse_expression_node(pair: Pair) -> FilesetParseResult { - assert_eq!(pair.as_rule(), Rule::expression); - static PRATT: LazyLock> = LazyLock::new(|| { - PrattParser::new() - .op(Op::infix(Rule::union_op, Assoc::Left)) - .op(Op::infix(Rule::intersection_op, Assoc::Left) - | Op::infix(Rule::difference_op, Assoc::Left)) - .op(Op::prefix(Rule::negate_op)) - }); - PRATT - .map_primary(parse_primary_node) - .map_prefix(|op, rhs| { - let op_kind = match op.as_rule() { - Rule::negate_op => UnaryOp::Negate, - r => panic!("unexpected prefix operator rule {r:?}"), - }; - let rhs = Box::new(rhs?); - let span = op.as_span().start_pos().span(&rhs.span.end_pos()); - let expr = ExpressionKind::Unary(op_kind, rhs); - Ok(ExpressionNode::new(expr, span)) - }) - .map_infix(|lhs, op, rhs| { - let op_kind = match op.as_rule() { - Rule::union_op => return Ok(union_nodes(lhs?, rhs?)), - Rule::intersection_op => BinaryOp::Intersection, - Rule::difference_op => BinaryOp::Difference, - r => panic!("unexpected infix operator rule {r:?}"), - }; - let lhs = Box::new(lhs?); - let rhs = Box::new(rhs?); - let span = lhs.span.start_pos().span(&rhs.span.end_pos()); - let expr = ExpressionKind::Binary(op_kind, lhs, rhs); - Ok(ExpressionNode::new(expr, span)) - }) - .parse(pair.into_inner()) -} - -/// Parses text into expression tree. No name resolution is made at this stage. -pub fn parse_program(text: &str) -> FilesetParseResult> { - let mut pairs = FilesetParser::parse(Rule::program, text)?; - let first = pairs.next().unwrap(); - parse_expression_node(first) -} - -/// Parses text into expression tree with bare string fallback. No name -/// resolution is made at this stage. -/// -/// If the text can't be parsed as a fileset expression, and if it doesn't -/// contain any operator-like characters, it will be parsed as a file path. -pub fn parse_program_or_bare_string(text: &str) -> FilesetParseResult> { - let mut pairs = FilesetParser::parse(Rule::program_or_bare_string, text)?; - let first = pairs.next().unwrap(); - let span = first.as_span(); - let expr = match first.as_rule() { - Rule::expression => return parse_expression_node(first), - Rule::bare_string_pattern => { - let [lhs, op, rhs] = first.into_inner().collect_array().unwrap(); - assert_eq!(lhs.as_rule(), Rule::strict_identifier); - assert_eq!(op.as_rule(), Rule::pattern_kind_op); - assert_eq!(rhs.as_rule(), Rule::bare_string); - let name_span = lhs.as_span(); - let value_span = rhs.as_span(); - let name = lhs.as_str(); - let value_expr = ExpressionKind::String(rhs.as_str().to_owned()); - let pattern = Box::new(PatternNode { - name, - name_span, - value: ExpressionNode::new(value_expr, value_span), - }); - ExpressionKind::Pattern(pattern) - } - Rule::bare_string => ExpressionKind::String(first.as_str().to_owned()), - r => panic!("unexpected program or bare string rule: {r:?}"), - }; - Ok(ExpressionNode::new(expr, span)) -} - -/// Map of fileset aliases. -pub type FilesetAliasesMap = AliasesMap; - -#[derive(Clone, Debug, Default)] -pub struct FilesetAliasParser; - -impl AliasDeclarationParser for FilesetAliasParser { - type Error = FilesetParseError; - - fn parse_declaration(&self, source: &str) -> Result { - let mut pairs = FilesetParser::parse(Rule::alias_declaration, source)?; - let first = pairs.next().unwrap(); - match first.as_rule() { - Rule::strict_identifier => Ok(AliasDeclaration::Symbol(first.as_str().to_owned())), - Rule::pattern_alias_declaration => { - let [name_pair, op, param_pair] = first.into_inner().collect_array().unwrap(); - assert_eq!(name_pair.as_rule(), Rule::strict_identifier); - assert_eq!(op.as_rule(), Rule::pattern_kind_op); - assert_eq!(param_pair.as_rule(), Rule::strict_identifier); - let name = name_pair.as_str().to_owned(); - let param = param_pair.as_str().to_owned(); - Ok(AliasDeclaration::Pattern(name, param)) - } - Rule::function_alias_declaration => { - let [name_pair, params_pair] = first.into_inner().collect_array().unwrap(); - assert_eq!(name_pair.as_rule(), Rule::function_name); - assert_eq!(params_pair.as_rule(), Rule::formal_parameters); - let name = name_pair.as_str().to_owned(); - let params_span = params_pair.as_span(); - let params = params_pair - .into_inner() - .map(|pair| match pair.as_rule() { - Rule::strict_identifier => pair.as_str().to_owned(), - r => panic!("unexpected formal parameter rule {r:?}"), - }) - .collect_vec(); - if params.iter().all_unique() { - Ok(AliasDeclaration::Function(name, params)) - } else { - Err(FilesetParseError::new( - FilesetParseErrorKind::RedefinedFunctionParameter, - params_span, - )) - } - } - r => panic!("unexpected alias declaration rule {r:?}"), - } - } -} - -impl AliasDefinitionParser for FilesetAliasParser { - type Output<'i> = ExpressionKind<'i>; - type Error = FilesetParseError; - - fn parse_definition<'i>(&self, source: &'i str) -> Result, Self::Error> { - parse_program(source) - } -} - -pub fn expand_aliases<'i>( - node: ExpressionNode<'i>, - aliases_map: &'i FilesetAliasesMap, -) -> FilesetParseResult> { - dsl_util::expand_aliases(node, aliases_map) -} - -pub(super) fn expect_string_literal<'a>( - type_name: &str, - node: &'a ExpressionNode<'_>, -) -> FilesetParseResult<&'a str> { - catch_aliases_no_diagnostics(node, |node| match &node.kind { - ExpressionKind::Identifier(name) => Ok(*name), - ExpressionKind::String(name) => Ok(name), - _ => Err(FilesetParseError::expression( - format!("Expected {type_name}"), - node.span, - )), - }) -} - -/// Applies the given function to the innermost `node` by unwrapping alias -/// expansion nodes. Appends alias expansion stack to error and diagnostics. -pub(super) fn catch_aliases<'a, 'i, T>( - diagnostics: &mut FilesetDiagnostics, - node: &'a ExpressionNode<'i>, - f: impl FnOnce(&mut FilesetDiagnostics, &'a ExpressionNode<'i>) -> Result, -) -> Result { - let (node, stack) = skip_aliases(node); - if stack.is_empty() { - f(diagnostics, node) - } else { - let mut inner_diagnostics = FilesetDiagnostics::new(); - let result = f(&mut inner_diagnostics, node); - diagnostics.extend_with(inner_diagnostics, |diag| attach_aliases_err(diag, &stack)); - result.map_err(|err| attach_aliases_err(err, &stack)) - } -} - -fn catch_aliases_no_diagnostics<'a, 'i, T>( - node: &'a ExpressionNode<'i>, - f: impl FnOnce(&'a ExpressionNode<'i>) -> Result, -) -> Result { - let (node, stack) = skip_aliases(node); - f(node).map_err(|err| attach_aliases_err(err, &stack)) -} - -fn skip_aliases<'a, 'i>( - mut node: &'a ExpressionNode<'i>, -) -> (&'a ExpressionNode<'i>, Vec<(AliasId<'i>, pest::Span<'i>)>) { - let mut stack = Vec::new(); - while let ExpressionKind::AliasExpanded(id, subst) = &node.kind { - stack.push((*id, node.span)); - node = subst; - } - (node, stack) -} - -fn attach_aliases_err( - err: FilesetParseError, - stack: &[(AliasId<'_>, pest::Span<'_>)], -) -> FilesetParseError { - stack - .iter() - .rfold(err, |err, &(id, span)| err.within_alias_expansion(id, span)) -} - -#[cfg(test)] -mod tests { - use assert_matches::assert_matches; - - use super::*; - use crate::dsl_util::KeywordArgument; - use crate::tests::TestResult; - - #[derive(Debug)] - struct WithFilesetAliasesMap { - aliases_map: FilesetAliasesMap, - } - - impl WithFilesetAliasesMap { - fn parse<'i>(&'i self, text: &'i str) -> FilesetParseResult> { - let node = parse_program(text)?; - expand_aliases(node, &self.aliases_map) - } - - fn parse_normalized<'i>(&'i self, text: &'i str) -> ExpressionNode<'i> { - normalize_tree(self.parse(text).unwrap()) - } - } - - fn with_aliases( - aliases: impl IntoIterator, impl Into)>, - ) -> WithFilesetAliasesMap { - let mut aliases_map = FilesetAliasesMap::new(); - for (decl, defn) in aliases { - aliases_map.insert(decl, defn, None).unwrap(); - } - WithFilesetAliasesMap { aliases_map } - } - - fn parse_into_kind(text: &str) -> Result, FilesetParseErrorKind> { - parse_program(text) - .map(|node| node.kind) - .map_err(|err| err.kind) - } - - fn parse_maybe_bare_into_kind(text: &str) -> Result, FilesetParseErrorKind> { - parse_program_or_bare_string(text) - .map(|node| node.kind) - .map_err(|err| err.kind) - } - - fn parse_normalized(text: &str) -> ExpressionNode<'_> { - normalize_tree(parse_program(text).unwrap()) - } - - fn parse_maybe_bare_normalized(text: &str) -> ExpressionNode<'_> { - normalize_tree(parse_program_or_bare_string(text).unwrap()) - } - - /// Drops auxiliary data from parsed tree so it can be compared with other. - fn normalize_tree(node: ExpressionNode) -> ExpressionNode { - fn empty_span() -> pest::Span<'static> { - pest::Span::new("", 0, 0).unwrap() - } - - fn normalize_list(nodes: Vec) -> Vec { - nodes.into_iter().map(normalize_tree).collect() - } - - fn normalize_function_call(function: FunctionCallNode) -> FunctionCallNode { - FunctionCallNode { - name: function.name, - name_span: empty_span(), - args: normalize_list(function.args), - keyword_args: function - .keyword_args - .into_iter() - .map(|arg| KeywordArgument { - name: arg.name, - name_span: empty_span(), - value: normalize_tree(arg.value), - }) - .collect(), - args_span: empty_span(), - } - } - - let normalized_kind = match node.kind { - ExpressionKind::Identifier(_) | ExpressionKind::String(_) => node.kind, - ExpressionKind::Pattern(pattern) => { - let pattern = Box::new(PatternNode { - name: pattern.name, - name_span: empty_span(), - value: normalize_tree(pattern.value), - }); - ExpressionKind::Pattern(pattern) - } - ExpressionKind::Unary(op, arg) => { - let arg = Box::new(normalize_tree(*arg)); - ExpressionKind::Unary(op, arg) - } - ExpressionKind::Binary(op, lhs, rhs) => { - let lhs = Box::new(normalize_tree(*lhs)); - let rhs = Box::new(normalize_tree(*rhs)); - ExpressionKind::Binary(op, lhs, rhs) - } - ExpressionKind::UnionAll(nodes) => { - let nodes = normalize_list(nodes); - ExpressionKind::UnionAll(nodes) - } - ExpressionKind::FunctionCall(function) => { - let function = Box::new(normalize_function_call(*function)); - ExpressionKind::FunctionCall(function) - } - ExpressionKind::AliasExpanded(_, subst) => normalize_tree(*subst).kind, - }; - ExpressionNode { - kind: normalized_kind, - span: empty_span(), - } - } - - #[test] - fn test_parse_tree_eq() { - assert_eq!( - parse_normalized(r#" foo( x ) | ~bar:"baz" "#), - parse_normalized(r#"(foo(x))|(~(bar:"baz"))"#) - ); - assert_ne!(parse_normalized(r#" foo "#), parse_normalized(r#" "foo" "#)); - } - - #[test] - fn test_parse_invalid_function_name() { - assert_eq!( - parse_into_kind("5foo(x)"), - Err(FilesetParseErrorKind::SyntaxError) - ); - } - - #[test] - fn test_parse_whitespace() { - let ascii_whitespaces: String = ('\x00'..='\x7f') - .filter(char::is_ascii_whitespace) - .collect(); - assert_eq!( - parse_normalized(&format!("{ascii_whitespaces}f()")), - parse_normalized("f()") - ); - } - - #[test] - fn test_parse_identifier() { - assert_eq!( - parse_into_kind("dir/foo-bar_0.baz"), - Ok(ExpressionKind::Identifier("dir/foo-bar_0.baz")) - ); - assert_eq!( - parse_into_kind("cli-reference@.md.snap"), - Ok(ExpressionKind::Identifier("cli-reference@.md.snap")) - ); - assert_eq!( - parse_into_kind("柔術.jj"), - Ok(ExpressionKind::Identifier("柔術.jj")) - ); - assert_eq!( - parse_into_kind(r#"Windows\Path"#), - Ok(ExpressionKind::Identifier(r#"Windows\Path"#)) - ); - assert_eq!( - parse_into_kind("glob*[chars]?"), - Ok(ExpressionKind::Identifier("glob*[chars]?")) - ); - } - - #[test] - fn test_parse_string_literal() { - // "\" escapes - assert_eq!( - parse_into_kind(r#" "\t\r\n\"\\\0\e" "#), - Ok(ExpressionKind::String("\t\r\n\"\\\0\u{1b}".to_owned())), - ); - - // Invalid "\" escape - assert_eq!( - parse_into_kind(r#" "\y" "#), - Err(FilesetParseErrorKind::SyntaxError), - ); - - // Single-quoted raw string - assert_eq!( - parse_into_kind(r#" '' "#), - Ok(ExpressionKind::String("".to_owned())), - ); - assert_eq!( - parse_into_kind(r#" 'a\n' "#), - Ok(ExpressionKind::String(r"a\n".to_owned())), - ); - assert_eq!( - parse_into_kind(r#" '\' "#), - Ok(ExpressionKind::String(r"\".to_owned())), - ); - assert_eq!( - parse_into_kind(r#" '"' "#), - Ok(ExpressionKind::String(r#"""#.to_owned())), - ); - - // Hex bytes - assert_eq!( - parse_into_kind(r#""\x61\x65\x69\x6f\x75""#), - Ok(ExpressionKind::String("aeiou".to_owned())), - ); - assert_eq!( - parse_into_kind(r#""\xe0\xe8\xec\xf0\xf9""#), - Ok(ExpressionKind::String("àèìðù".to_owned())), - ); - assert_eq!( - parse_into_kind(r#""\x""#), - Err(FilesetParseErrorKind::SyntaxError), - ); - assert_eq!( - parse_into_kind(r#""\xf""#), - Err(FilesetParseErrorKind::SyntaxError), - ); - assert_eq!( - parse_into_kind(r#""\xgg""#), - Err(FilesetParseErrorKind::SyntaxError), - ); - } - - #[test] - fn test_parse_pattern() -> TestResult { - fn unwrap_pattern(kind: ExpressionKind<'_>) -> (&str, ExpressionKind<'_>) { - match kind { - ExpressionKind::Pattern(pattern) => (pattern.name, pattern.value.kind), - _ => panic!("unexpected expression: {kind:?}"), - } - } - - assert_eq!( - unwrap_pattern(parse_into_kind(r#" foo:bar "#)?), - ("foo", ExpressionKind::Identifier("bar")) - ); - assert_eq!( - unwrap_pattern(parse_into_kind(" foo:glob*[chars]? ")?), - ("foo", ExpressionKind::Identifier("glob*[chars]?")) - ); - assert_eq!( - unwrap_pattern(parse_into_kind(r#" foo:"bar" "#)?), - ("foo", ExpressionKind::String("bar".to_owned())) - ); - assert_eq!( - unwrap_pattern(parse_into_kind(r#" foo:"" "#)?), - ("foo", ExpressionKind::String("".to_owned())) - ); - assert_eq!( - unwrap_pattern(parse_into_kind(r#" foo:'\' "#)?), - ("foo", ExpressionKind::String(r"\".to_owned())) - ); - assert_eq!( - parse_into_kind(r#" foo: "#), - Err(FilesetParseErrorKind::SyntaxError) - ); - - // Whitespace isn't allowed in between - assert_eq!( - parse_into_kind(r#" foo: "" "#), - Err(FilesetParseErrorKind::SyntaxError) - ); - assert_eq!( - parse_into_kind(r#" foo :"" "#), - Err(FilesetParseErrorKind::SyntaxError) - ); - // Whitespace is allowed in parenthesized value expression - assert_eq!( - parse_normalized("foo:( 'bar' )"), - parse_normalized("foo:'bar'") - ); - - // Functions are allowed - assert_eq!(parse_normalized("x:f(y)"), parse_normalized("x:(f(y))")); - // Logical operators have lower binding strength - assert_eq!(parse_normalized("x:y&z"), parse_normalized("(x:y)&(z)")); - assert_matches!( - parse_into_kind("x:~y"), // (x:) ~ (y) - Err(FilesetParseErrorKind::SyntaxError) - ); - - // Pattern prefix is like (type)x cast, so is evaluated from right - assert_eq!(parse_normalized("x:y:z"), parse_normalized("x:(y:z)")); - Ok(()) - } - - #[test] - fn test_parse_operator() -> TestResult { - assert_matches!( - parse_into_kind("~x"), - Ok(ExpressionKind::Unary(UnaryOp::Negate, _)) - ); - assert_matches!( - parse_into_kind("x|y"), - Ok(ExpressionKind::UnionAll(nodes)) if nodes.len() == 2 - ); - assert_matches!( - parse_into_kind("x|y|z"), - Ok(ExpressionKind::UnionAll(nodes)) if nodes.len() == 3 - ); - assert_matches!( - parse_into_kind("x&y"), - Ok(ExpressionKind::Binary(BinaryOp::Intersection, _, _)) - ); - assert_matches!( - parse_into_kind("x~y"), - Ok(ExpressionKind::Binary(BinaryOp::Difference, _, _)) - ); - - // Set operator associativity/precedence - assert_eq!(parse_normalized("~x|y"), parse_normalized("(~x)|y")); - assert_eq!(parse_normalized("x&~y"), parse_normalized("x&(~y)")); - assert_eq!(parse_normalized("x~~y"), parse_normalized("x~(~y)")); - assert_eq!(parse_normalized("x~~~y"), parse_normalized("x~(~(~y))")); - assert_eq!(parse_normalized("x|y|z"), parse_normalized("(x|y)|z")); - assert_eq!(parse_normalized("x&y|z"), parse_normalized("(x&y)|z")); - assert_eq!(parse_normalized("x|y&z"), parse_normalized("x|(y&z)")); - assert_eq!(parse_normalized("x|y~z"), parse_normalized("x|(y~z)")); - assert_eq!(parse_normalized("~x:y"), parse_normalized("~(x:y)")); - assert_eq!(parse_normalized("x|y:z"), parse_normalized("x|(y:z)")); - - // Expression span - assert_eq!(parse_program(" ~ x ")?.span.as_str(), "~ x"); - assert_eq!(parse_program(" x |y ")?.span.as_str(), "x |y"); - assert_eq!(parse_program(" (x) ")?.span.as_str(), "(x)"); - assert_eq!(parse_program("~( x|y) ")?.span.as_str(), "~( x|y)"); - Ok(()) - } - - #[test] - fn test_parse_function_call() -> TestResult { - fn unwrap_function_call(node: ExpressionNode<'_>) -> Box> { - match node.kind { - ExpressionKind::FunctionCall(function) => function, - _ => panic!("unexpected expression: {node:?}"), - } - } - - assert_matches!( - parse_into_kind("foo()"), - Ok(ExpressionKind::FunctionCall(_)) - ); - - // Trailing comma isn't allowed for empty argument - assert!(parse_into_kind("foo(,)").is_err()); - - // Trailing comma is allowed for the last argument - assert_eq!(parse_normalized("foo(a,)"), parse_normalized("foo(a)")); - assert_eq!(parse_normalized("foo(a , )"), parse_normalized("foo(a)")); - assert!(parse_into_kind("foo(,a)").is_err()); - assert!(parse_into_kind("foo(a,,)").is_err()); - assert!(parse_into_kind("foo(a , , )").is_err()); - assert_eq!(parse_normalized("foo(a,b,)"), parse_normalized("foo(a,b)")); - assert!(parse_into_kind("foo(a,,b)").is_err()); - - // Expression span - let function = unwrap_function_call(parse_program("foo( a, (b) , ~(c) )")?); - assert_eq!(function.name_span.as_str(), "foo"); - assert_eq!(function.args_span.as_str(), "a, (b) , ~(c)"); - assert_eq!(function.args[0].span.as_str(), "a"); - assert_eq!(function.args[1].span.as_str(), "(b)"); - assert_eq!(function.args[2].span.as_str(), "~(c)"); - Ok(()) - } - - #[test] - fn test_parse_bare_string() -> TestResult { - fn unwrap_pattern(kind: ExpressionKind<'_>) -> (&str, ExpressionKind<'_>) { - match kind { - ExpressionKind::Pattern(pattern) => (pattern.name, pattern.value.kind), - _ => panic!("unexpected expression: {kind:?}"), - } - } - - // Valid expression should be parsed as such - assert_eq!( - parse_maybe_bare_into_kind(" valid "), - Ok(ExpressionKind::Identifier("valid")) - ); - assert_eq!( - parse_maybe_bare_normalized("f(x)&y"), - parse_normalized("f(x)&y") - ); - assert_eq!( - unwrap_pattern(parse_maybe_bare_into_kind("foo:bar")?), - ("foo", ExpressionKind::Identifier("bar")) - ); - - // Bare string - assert_eq!( - parse_maybe_bare_into_kind("Foo Bar.txt"), - Ok(ExpressionKind::String("Foo Bar.txt".to_owned())) - ); - assert_eq!( - parse_maybe_bare_into_kind(r#"Windows\Path with space"#), - Ok(ExpressionKind::String( - r#"Windows\Path with space"#.to_owned() - )) - ); - assert_eq!( - parse_maybe_bare_into_kind("柔 術 . j j"), - Ok(ExpressionKind::String("柔 術 . j j".to_owned())) - ); - assert_eq!( - parse_maybe_bare_into_kind("Unicode emoji 💩"), - Ok(ExpressionKind::String("Unicode emoji 💩".to_owned())) - ); - assert_eq!( - parse_maybe_bare_into_kind("looks like & expression"), - Err(FilesetParseErrorKind::SyntaxError) - ); - assert_eq!( - parse_maybe_bare_into_kind("unbalanced_parens("), - Err(FilesetParseErrorKind::SyntaxError) - ); - - // Bare string pattern - assert_eq!( - unwrap_pattern(parse_maybe_bare_into_kind("foo: bar baz")?), - ("foo", ExpressionKind::String(" bar baz".to_owned())) - ); - assert_eq!( - unwrap_pattern(parse_maybe_bare_into_kind("foo:glob * [chars]?")?), - ("foo", ExpressionKind::String("glob * [chars]?".to_owned())) - ); - assert_eq!( - parse_maybe_bare_into_kind("foo: bar:baz"), - Err(FilesetParseErrorKind::SyntaxError) - ); - assert_eq!( - parse_maybe_bare_into_kind("foo:"), - Err(FilesetParseErrorKind::SyntaxError) - ); - assert_eq!( - parse_maybe_bare_into_kind(r#"foo:"unclosed quote"#), - Err(FilesetParseErrorKind::SyntaxError) - ); - - // Surrounding spaces are simply preserved. They could be trimmed, but - // space is valid bare_string character. - assert_eq!( - parse_maybe_bare_into_kind(" No trim "), - Ok(ExpressionKind::String(" No trim ".to_owned())) - ); - Ok(()) - } - - #[test] - fn test_parse_error() { - insta::assert_snapshot!(parse_program("foo|").unwrap_err().to_string(), @" - --> 1:5 - | - 1 | foo| - | ^--- - | - = expected `~` or - "); - } - - #[test] - fn test_parse_alias_symbol_decl() -> TestResult { - let mut aliases_map = FilesetAliasesMap::new(); - aliases_map.insert("sym", "symbol", None)?; - assert_eq!(aliases_map.symbol_names().count(), 1); - let (id, defn, _doc) = aliases_map.get_symbol("sym").unwrap(); - assert_eq!(id, AliasId::Symbol("sym")); - assert_eq!(defn, "symbol"); - - // Non-ASCII character isn't allowed in alias symbol. This rule can be - // relaxed if needed. - assert!(aliases_map.insert("柔術", "none()", None).is_err()); - Ok(()) - } - - #[test] - fn test_parse_alias_pattern_decl() -> TestResult { - let mut aliases_map = FilesetAliasesMap::new(); - assert!(aliases_map.insert("pat:", "bad_pattern", None).is_err()); - aliases_map.insert("pat:a", "pattern_a", None)?; - aliases_map.insert("pat:b", "pattern_b", None)?; - assert_eq!(aliases_map.pattern_names().count(), 1); - let (id, param, defn, _doc) = aliases_map.get_pattern("pat").unwrap(); - assert_eq!(id, AliasId::Pattern("pat", "b")); - assert_eq!(param, "b"); - assert_eq!(defn, "pattern_b"); - - // Non-ASCII character isn't allowed. This rule can be relaxed if - // needed. - assert!(aliases_map.insert("柔術:x", "none()", None).is_err()); - assert!(aliases_map.insert("x:柔術", "none()", None).is_err()); - Ok(()) - } - - #[test] - fn test_parse_alias_func_decl() -> TestResult { - let mut aliases_map = FilesetAliasesMap::new(); - assert!(aliases_map.insert("5func()", "bad_function", None).is_err()); - aliases_map.insert("func()", "function_0", None)?; - aliases_map.insert("func(a)", "function_1a", None)?; - aliases_map.insert("func(b)", "function_1b", None)?; - aliases_map.insert("func(a, b)", "function_2", None)?; - assert_eq!(aliases_map.function_names().count(), 1); - - let (id, params, defn, _doc) = aliases_map.get_function("func", 0).unwrap(); - assert_eq!(id, AliasId::Function("func", &[])); - assert!(params.is_empty()); - assert_eq!(defn, "function_0"); - - let (id, params, defn, _doc) = aliases_map.get_function("func", 1).unwrap(); - assert_eq!(id, AliasId::Function("func", &["b".to_owned()])); - assert_eq!(params, ["b"]); - assert_eq!(defn, "function_1b"); - - let (id, params, defn, _doc) = aliases_map.get_function("func", 2).unwrap(); - assert_eq!( - id, - AliasId::Function("func", &["a".to_owned(), "b".to_owned()]) - ); - assert_eq!(params, ["a", "b"]); - assert_eq!(defn, "function_2"); - - assert!(aliases_map.get_function("func", 3).is_none()); - Ok(()) - } - - #[test] - fn test_parse_alias_formal_parameter() { - let mut aliases_map = FilesetAliasesMap::new(); - // Formal parameter 'a' can't be redefined - assert_eq!( - aliases_map.insert("f(a, a)", "bad", None).unwrap_err().kind, - FilesetParseErrorKind::RedefinedFunctionParameter - ); - // Trailing comma isn't allowed for empty parameter - assert!(aliases_map.insert("f(,)", "bad", None).is_err()); - // Trailing comma is allowed for the last parameter - assert!(aliases_map.insert("g(a,)", "bad", None).is_ok()); - assert!(aliases_map.insert("h(a , )", "bad", None).is_ok()); - assert!(aliases_map.insert("i(,a)", "bad", None).is_err()); - assert!(aliases_map.insert("j(a,,)", "bad", None).is_err()); - assert!(aliases_map.insert("k(a , , )", "bad", None).is_err()); - assert!(aliases_map.insert("l(a,b,)", "bad", None).is_ok()); - assert!(aliases_map.insert("m(a,,b)", "bad", None).is_err()); - } - - #[test] - fn test_expand_symbol_alias() { - assert_eq!( - with_aliases([("AB", "a&b")]).parse_normalized("AB|c"), - parse_normalized("(a&b)|c") - ); - assert_eq!( - with_aliases([("AB", "a|b")]).parse_normalized("AB~f(AB)"), - parse_normalized("(a|b)~f(a|b)") - ); - - // Not string substitution 'a&b|c', but tree substitution. - assert_eq!( - with_aliases([("BC", "b|c")]).parse_normalized("a&BC"), - parse_normalized("a&(b|c)") - ); - - // String literal should not be substituted with alias. - assert_eq!( - with_aliases([("A", "a")]).parse_normalized(r#"A|"A"|'A'"#), - parse_normalized("a|'A'|'A'") - ); - - // Kind of string pattern should not be substituted, which is similar to - // function name. - assert_eq!( - with_aliases([("A", "a")]).parse_normalized("A:b"), - parse_normalized("A:b") - ); - - // Value of string pattern can be substituted if it's an identifier. - assert_eq!( - with_aliases([("A", "a")]).parse_normalized("p:A"), - parse_normalized("p:a") - ); - assert_eq!( - with_aliases([("A", "a")]).parse_normalized("p:'A'"), - parse_normalized("p:'A'") - ); - - // Multi-level substitution. - assert_eq!( - with_aliases([("A", "BC"), ("BC", "b|C"), ("C", "c")]).parse_normalized("A"), - parse_normalized("b|c") - ); - - // Infinite recursion, where the top-level error isn't of RecursiveAlias kind. - assert_eq!( - with_aliases([("A", "A")]).parse("A").unwrap_err().kind, - FilesetParseErrorKind::InAliasExpansion("A".to_owned()) - ); - assert_eq!( - with_aliases([("A", "B"), ("B", "b|C"), ("C", "c|B")]) - .parse("A") - .unwrap_err() - .kind, - FilesetParseErrorKind::InAliasExpansion("A".to_owned()) - ); - - // Error in alias definition. - assert_eq!( - with_aliases([("A", "a(")]).parse("A").unwrap_err().kind, - FilesetParseErrorKind::InAliasExpansion("A".to_owned()) - ); - } - - #[test] - fn test_expand_pattern_alias() { - assert_eq!( - with_aliases([("P:x", "x")]).parse_normalized("P:a"), - parse_normalized("a") - ); - - // Argument should be resolved in the current scope. - assert_eq!( - with_aliases([("P:x", "x|a")]).parse_normalized("P:x"), - parse_normalized("x|a") - ); - // P:a -> (Q:a)&y -> (x|a)&y - assert_eq!( - with_aliases([("P:x", "(Q:x)&y"), ("Q:y", "x|y")]).parse_normalized("P:a"), - parse_normalized("(x|a)&y") - ); - - // Pattern parameter should precede the symbol alias. - assert_eq!( - with_aliases([("P:X", "X"), ("X", "x")]).parse_normalized("(P:a)|X"), - parse_normalized("a|x") - ); - - // Pattern parameter shouldn't be expanded in symbol alias. - assert_eq!( - with_aliases([("P:x", "x|A"), ("A", "x")]).parse_normalized("P:a"), - parse_normalized("a|x") - ); - - // String literal should not be substituted with pattern parameter. - assert_eq!( - with_aliases([("P:x", "x|'x'")]).parse_normalized("P:a"), - parse_normalized("a|'x'") - ); - - // Pattern and symbol aliases reside in separate namespaces. - assert_eq!( - with_aliases([("A:x", "A"), ("A", "a")]).parse_normalized("A:x"), - parse_normalized("a") - ); - - // Infinite recursion, where the top-level error isn't of RecursiveAlias kind. - assert_eq!( - with_aliases([("P:x", "Q:x"), ("Q:x", "R:x"), ("R:x", "P:x")]) - .parse("P:a") - .unwrap_err() - .kind, - FilesetParseErrorKind::InAliasExpansion("P:x".to_owned()) - ); - } - - #[test] - fn test_expand_function_alias() { - assert_eq!( - with_aliases([("F( )", "a")]).parse_normalized("F()"), - parse_normalized("a") - ); - assert_eq!( - with_aliases([("F( x )", "x")]).parse_normalized("F(a)"), - parse_normalized("a") - ); - assert_eq!( - with_aliases([("F( x, y )", "x|y")]).parse_normalized("F(a, b)"), - parse_normalized("a|b") - ); - - // Not recursion because functions are overloaded by arity. - assert_eq!( - with_aliases([("F(x)", "F(x,b)"), ("F(x,y)", "x|y")]).parse_normalized("F(a)"), - parse_normalized("a|b") - ); - - // Arguments should be resolved in the current scope. - assert_eq!( - with_aliases([("F(x,y)", "x|y")]).parse_normalized("F(a~y,b~x)"), - parse_normalized("(a~y)|(b~x)") - ); - // F(a) -> G(a)&y -> (x|a)&y - assert_eq!( - with_aliases([("F(x)", "G(x)&y"), ("G(y)", "x|y")]).parse_normalized("F(a)"), - parse_normalized("(x|a)&y") - ); - // F(G(a)) -> F(x|a) -> G(x|a)&y -> (x|(x|a))&y - assert_eq!( - with_aliases([("F(x)", "G(x)&y"), ("G(y)", "x|y")]).parse_normalized("F(G(a))"), - parse_normalized("(x|(x|a))&y") - ); - - // Function parameter should precede the symbol alias. - assert_eq!( - with_aliases([("F(X)", "X"), ("X", "x")]).parse_normalized("F(a)|X"), - parse_normalized("a|x") - ); - - // Function parameter shouldn't be expanded in symbol alias. - assert_eq!( - with_aliases([("F(x)", "x|A"), ("A", "x")]).parse_normalized("F(a)"), - parse_normalized("a|x") - ); - - // String literal should not be substituted with function parameter. - assert_eq!( - with_aliases([("F(x)", "x|'x'")]).parse_normalized("F(a)"), - parse_normalized("a|'x'") - ); - - // Function and symbol aliases reside in separate namespaces. - assert_eq!( - with_aliases([("A()", "A"), ("A", "a")]).parse_normalized("A()"), - parse_normalized("a") - ); - - // Invalid number of arguments. - assert_eq!( - with_aliases([("F()", "x")]).parse("F(a)").unwrap_err().kind, - FilesetParseErrorKind::InvalidArguments { - name: "F".to_owned(), - message: "Expected 0 arguments".to_owned() - } - ); - assert_eq!( - with_aliases([("F(x)", "x")]).parse("F()").unwrap_err().kind, - FilesetParseErrorKind::InvalidArguments { - name: "F".to_owned(), - message: "Expected 1 arguments".to_owned() - } - ); - assert_eq!( - with_aliases([("F(x,y)", "x|y")]) - .parse("F(a,b,c)") - .unwrap_err() - .kind, - FilesetParseErrorKind::InvalidArguments { - name: "F".to_owned(), - message: "Expected 2 arguments".to_owned() - } - ); - assert_eq!( - with_aliases([("F(x)", "x"), ("F(x,y)", "x|y")]) - .parse("F()") - .unwrap_err() - .kind, - FilesetParseErrorKind::InvalidArguments { - name: "F".to_owned(), - message: "Expected 1 to 2 arguments".to_owned() - } - ); - assert_eq!( - with_aliases([("F()", "x"), ("F(x,y)", "x|y")]) - .parse("F(a)") - .unwrap_err() - .kind, - FilesetParseErrorKind::InvalidArguments { - name: "F".to_owned(), - message: "Expected 0, 2 arguments".to_owned() - } - ); - - // Infinite recursion, where the top-level error isn't of RecursiveAlias kind. - assert_eq!( - with_aliases([("F(x)", "G(x)"), ("G(x)", "H(x)"), ("H(x)", "F(x)")]) - .parse("F(a)") - .unwrap_err() - .kind, - FilesetParseErrorKind::InAliasExpansion("F(x)".to_owned()) - ); - assert_eq!( - with_aliases([("F(x)", "F(x,b)"), ("F(x,y)", "F(x|y)")]) - .parse("F(a)") - .unwrap_err() - .kind, - FilesetParseErrorKind::InAliasExpansion("F(x)".to_owned()) - ); - } -} +// Allow unused imports here, because all symbols moved to the core crate and we need to +// continue to reexport the same APIs. +#![expect(unused_imports)] + +pub use jj_core::fileset_parser::BinaryOp; +pub use jj_core::fileset_parser::ExpressionKind; +pub use jj_core::fileset_parser::ExpressionNode; +pub use jj_core::fileset_parser::FilesetAliasParser; +pub use jj_core::fileset_parser::FilesetAliasesMap; +pub use jj_core::fileset_parser::FilesetDiagnostics; +pub use jj_core::fileset_parser::FilesetParseError; +pub use jj_core::fileset_parser::FilesetParseErrorKind; +pub use jj_core::fileset_parser::FilesetParseResult; +pub use jj_core::fileset_parser::FunctionCallNode; +pub use jj_core::fileset_parser::PatternNode; +pub use jj_core::fileset_parser::Rule; +pub use jj_core::fileset_parser::UnaryOp; +pub use jj_core::fileset_parser::catch_aliases; +pub use jj_core::fileset_parser::expand_aliases; +pub use jj_core::fileset_parser::expect_string_literal; +pub use jj_core::fileset_parser::parse_program; +pub use jj_core::fileset_parser::parse_program_or_bare_string; diff --git a/lib/src/lib.rs b/lib/src/lib.rs index 60517104774..9b30fd61e22 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -67,7 +67,7 @@ pub mod gitignore; pub mod gpg_signing; pub mod graph; pub mod graph_dominators; -pub mod hex_util; +pub use jj_core::hex_util; pub mod id_prefix; pub mod index; pub mod iter_util; @@ -84,7 +84,7 @@ pub mod op_walk; pub mod operation; #[expect(missing_docs)] pub mod protos; -pub mod ref_name; +pub use jj_core::ref_name; pub mod refs; pub mod repo; pub mod repo_path; diff --git a/lib/src/matchers.rs b/lib/src/matchers.rs index 10cc901064a..e2fa830d87c 100644 --- a/lib/src/matchers.rs +++ b/lib/src/matchers.rs @@ -19,116 +19,17 @@ use std::fmt::Debug; use globset::Glob; use itertools::Itertools as _; +pub use jj_core::matchers::EverythingMatcher; +pub use jj_core::matchers::Matcher; +pub use jj_core::matchers::NothingMatcher; +pub use jj_core::matchers::Visit; +pub use jj_core::matchers::VisitDirs; +pub use jj_core::matchers::VisitFiles; use tracing::instrument; use crate::repo_path::RepoPath; -use crate::repo_path::RepoPathComponentBuf; use crate::repo_path::RepoPathTree; -#[derive(PartialEq, Eq, Debug)] -pub enum Visit { - /// Everything in the directory is *guaranteed* to match, no need to check - /// descendants - AllRecursively, - Specific { - dirs: VisitDirs, - files: VisitFiles, - }, - /// Nothing in the directory or its subdirectories will match. - /// - /// This is the same as `Specific` with no directories or files. Use - /// `Visit::set()` to get create an instance that's `Specific` or - /// `Nothing` depending on the values at runtime. - Nothing, -} - -impl Visit { - /// All entries in the directory need to be visited, but they are not - /// guaranteed to match. - const SOME: Self = Self::Specific { - dirs: VisitDirs::All, - files: VisitFiles::All, - }; - - fn sets(dirs: HashSet, files: HashSet) -> Self { - if dirs.is_empty() && files.is_empty() { - Self::Nothing - } else { - Self::Specific { - dirs: VisitDirs::Set(dirs), - files: VisitFiles::Set(files), - } - } - } - - pub fn is_nothing(&self) -> bool { - *self == Self::Nothing - } -} - -#[derive(PartialEq, Eq, Debug)] -pub enum VisitDirs { - All, - Set(HashSet), -} - -#[derive(PartialEq, Eq, Debug)] -pub enum VisitFiles { - All, - Set(HashSet), -} - -pub trait Matcher: Debug + Send + Sync { - fn matches(&self, file: &RepoPath) -> bool; - fn visit(&self, dir: &RepoPath) -> Visit; -} - -impl Matcher for &T { - fn matches(&self, file: &RepoPath) -> bool { - ::matches(self, file) - } - - fn visit(&self, dir: &RepoPath) -> Visit { - ::visit(self, dir) - } -} - -impl Matcher for Box { - fn matches(&self, file: &RepoPath) -> bool { - ::matches(self, file) - } - - fn visit(&self, dir: &RepoPath) -> Visit { - ::visit(self, dir) - } -} - -#[derive(PartialEq, Eq, Debug)] -pub struct NothingMatcher; - -impl Matcher for NothingMatcher { - fn matches(&self, _file: &RepoPath) -> bool { - false - } - - fn visit(&self, _dir: &RepoPath) -> Visit { - Visit::Nothing - } -} - -#[derive(PartialEq, Eq, Debug)] -pub struct EverythingMatcher; - -impl Matcher for EverythingMatcher { - fn matches(&self, _file: &RepoPath) -> bool { - true - } - - fn visit(&self, _dir: &RepoPath) -> Visit { - Visit::AllRecursively - } -} - #[derive(PartialEq, Eq, Debug)] pub struct FilesMatcher { tree: RepoPathTree, @@ -527,6 +428,7 @@ mod tests { use super::*; use crate::fileset::parse_file_glob; + use crate::repo_path::RepoPathComponentBuf; fn repo_path(value: &str) -> &RepoPath { RepoPath::from_internal_string(value).unwrap() diff --git a/lib/src/object_id.rs b/lib/src/object_id.rs index 24e513e257b..b1a6fb23eb2 100644 --- a/lib/src/object_id.rs +++ b/lib/src/object_id.rs @@ -12,323 +12,11 @@ // See the License for the specific language governing permissions and // limitations under the License. -#![expect(missing_docs)] - -use std::fmt; -use std::fmt::Debug; - -use crate::hex_util; - -pub trait ObjectId { - fn object_type(&self) -> String; - fn as_bytes(&self) -> &[u8]; - fn to_bytes(&self) -> Vec; - fn hex(&self) -> String; -} - -// Defines a new struct type with visibility `vis` and name `ident` containing -// a single Vec used to store an identifier (typically the output of a hash -// function) as bytes. Types defined using this macro automatically implement -// the `ObjectId` and `ContentHash` traits. -// Documentation comments written inside the macro definition will be captured -// and associated with the type defined by the macro. -// -// Example: -// ```no_run -// id_type!( -// /// My favorite id type. -// pub MyId { hex() } -// ); -// ``` -macro_rules! id_type { - ( $(#[$attr:meta])* - $vis:vis $name:ident { $hex_method:ident() } - ) => { - $(#[$attr])* - #[derive($crate::content_hash::ContentHash, PartialEq, Eq, PartialOrd, Ord, Clone, Hash)] - $vis struct $name(Vec); - $crate::object_id::impl_id_type!($name, $hex_method); - }; -} - -macro_rules! impl_id_type { - ($name:ident, $hex_method:ident) => { - #[allow(dead_code)] - impl $name { - /// Creates a new instance of this id type from the given bytes. - pub fn new(value: Vec) -> Self { - Self(value) - } - - /// Creates a new instance of this id type from the given byte slice. - pub fn from_bytes(bytes: &[u8]) -> Self { - Self(bytes.to_vec()) - } - - /// Parses the given hex string into an ObjectId. - /// - /// The given string must be valid. A static str is required to - /// prevent API misuse. - pub fn from_hex(hex: &'static str) -> Self { - Self::try_from_hex(hex).unwrap() - } - - /// Parses the given hex string into an ObjectId. - pub fn try_from_hex(hex: impl AsRef<[u8]>) -> Option { - $crate::hex_util::decode_hex(hex).map(Self) - } - } - - impl std::fmt::Debug for $name { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { - // TODO: should we use $hex_method here? - f.debug_tuple(stringify!($name)).field(&self.hex()).finish() - } - } - - impl std::fmt::Display for $name { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { - f.pad(&self.$hex_method()) - } - } - - impl serde::Serialize for $name { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - if serializer.is_human_readable() { - self.$hex_method().serialize(serializer) - } else { - self.as_bytes().serialize(serializer) - } - } - } - - impl crate::object_id::ObjectId for $name { - fn object_type(&self) -> String { - stringify!($name) - .strip_suffix("Id") - .unwrap() - .to_ascii_lowercase() - .to_string() - } - - fn as_bytes(&self) -> &[u8] { - &self.0 - } - - fn to_bytes(&self) -> Vec { - self.0.clone() - } - - fn hex(&self) -> String { - $crate::hex_util::encode_hex(&self.0) - } - } - }; -} - -pub(crate) use id_type; -pub(crate) use impl_id_type; - -/// An identifier prefix (typically from a type implementing the [`ObjectId`] -/// trait) with facilities for converting between bytes and a hex string. -#[derive(Clone, PartialEq, Eq)] -pub struct HexPrefix { - // For odd-length prefixes, the lower 4 bits of the last byte are - // zero-filled (e.g. the prefix "abc" is stored in two bytes as "abc0"). - min_prefix_bytes: Vec, - has_odd_byte: bool, -} - -impl HexPrefix { - /// Returns a new `HexPrefix` or `None` if `prefix` cannot be decoded from - /// hex to bytes. - pub fn try_from_hex(prefix: impl AsRef<[u8]>) -> Option { - let (min_prefix_bytes, has_odd_byte) = hex_util::decode_hex_prefix(prefix)?; - Some(Self { - min_prefix_bytes, - has_odd_byte, - }) - } - - /// Returns a new `HexPrefix` or `None` if `prefix` cannot be decoded from - /// "reverse" hex to bytes. - pub fn try_from_reverse_hex(prefix: impl AsRef<[u8]>) -> Option { - let (min_prefix_bytes, has_odd_byte) = hex_util::decode_reverse_hex_prefix(prefix)?; - Some(Self { - min_prefix_bytes, - has_odd_byte, - }) - } - - pub fn from_bytes(bytes: &[u8]) -> Self { - Self { - min_prefix_bytes: bytes.to_owned(), - has_odd_byte: false, - } - } - - /// Returns a new `HexPrefix` representing the given `id`. - pub fn from_id(id: &T) -> Self { - Self::from_bytes(id.as_bytes()) - } - - /// Returns string representation of this prefix using hex digits. - pub fn hex(&self) -> String { - let mut hex_string = hex_util::encode_hex(&self.min_prefix_bytes); - if self.has_odd_byte { - hex_string.pop().unwrap(); - } - hex_string - } - - /// Returns string representation of this prefix using `z-k` "digits". - pub fn reverse_hex(&self) -> String { - let mut hex_string = hex_util::encode_reverse_hex(&self.min_prefix_bytes); - if self.has_odd_byte { - hex_string.pop().unwrap(); - } - hex_string - } - - /// Minimum bytes that would match this prefix. (e.g. "abc0" for "abc") - /// - /// Use this to partition a sorted slice, and test `matches(id)` from there. - pub fn min_prefix_bytes(&self) -> &[u8] { - &self.min_prefix_bytes - } - - /// Returns the bytes representation if this prefix can be a full id. - pub fn as_full_bytes(&self) -> Option<&[u8]> { - (!self.has_odd_byte).then_some(&self.min_prefix_bytes) - } - - fn split_odd_byte(&self) -> (Option, &[u8]) { - if self.has_odd_byte { - let (&odd, prefix) = self.min_prefix_bytes.split_last().unwrap(); - (Some(odd), prefix) - } else { - (None, &self.min_prefix_bytes) - } - } - - /// Returns whether the stored prefix matches the prefix of `id`. - pub fn matches(&self, id: &Q) -> bool { - let id_bytes = id.as_bytes(); - let (maybe_odd, prefix) = self.split_odd_byte(); - if id_bytes.starts_with(prefix) { - if let Some(odd) = maybe_odd { - matches!(id_bytes.get(prefix.len()), Some(v) if v & 0xf0 == odd) - } else { - true - } - } else { - false - } - } -} - -impl Debug for HexPrefix { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { - f.debug_tuple("HexPrefix").field(&self.hex()).finish() - } -} - -/// The result of a prefix search. -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum PrefixResolution { - NoMatch, - SingleMatch(T), - AmbiguousMatch, -} - -impl PrefixResolution { - pub fn map(self, f: impl FnOnce(T) -> U) -> PrefixResolution { - match self { - Self::NoMatch => PrefixResolution::NoMatch, - Self::SingleMatch(x) => PrefixResolution::SingleMatch(f(x)), - Self::AmbiguousMatch => PrefixResolution::AmbiguousMatch, - } - } - - pub fn filter_map(self, f: impl FnOnce(T) -> Option) -> PrefixResolution { - match self { - Self::NoMatch => PrefixResolution::NoMatch, - Self::SingleMatch(x) => match f(x) { - None => PrefixResolution::NoMatch, - Some(y) => PrefixResolution::SingleMatch(y), - }, - Self::AmbiguousMatch => PrefixResolution::AmbiguousMatch, - } - } -} - -impl PrefixResolution { - pub fn plus(&self, other: &Self) -> Self { - match (self, other) { - (Self::NoMatch, other) => other.clone(), - (local, Self::NoMatch) => local.clone(), - (Self::AmbiguousMatch, _) => Self::AmbiguousMatch, - (_, Self::AmbiguousMatch) => Self::AmbiguousMatch, - (Self::SingleMatch(_), Self::SingleMatch(_)) => Self::AmbiguousMatch, - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::backend::ChangeId; - use crate::backend::CommitId; - - #[test] - fn test_display_object_id() { - let commit_id = CommitId::from_hex("deadbeef0123"); - assert_eq!(format!("{commit_id}"), "deadbeef0123"); - assert_eq!(format!("{commit_id:.6}"), "deadbe"); - - let change_id = ChangeId::from_hex("deadbeef0123"); - assert_eq!(format!("{change_id}"), "mlpmollkzyxw"); - assert_eq!(format!("{change_id:.6}"), "mlpmol"); - } - - #[test] - fn test_hex_prefix_prefixes() { - let prefix = HexPrefix::try_from_hex("").unwrap(); - assert_eq!(prefix.min_prefix_bytes(), b""); - - let prefix = HexPrefix::try_from_hex("1").unwrap(); - assert_eq!(prefix.min_prefix_bytes(), b"\x10"); - - let prefix = HexPrefix::try_from_hex("12").unwrap(); - assert_eq!(prefix.min_prefix_bytes(), b"\x12"); - - let prefix = HexPrefix::try_from_hex("123").unwrap(); - assert_eq!(prefix.min_prefix_bytes(), b"\x12\x30"); - - let bad_prefix = HexPrefix::try_from_hex("0x123"); - assert_eq!(bad_prefix, None); - - let bad_prefix = HexPrefix::try_from_hex("foobar"); - assert_eq!(bad_prefix, None); - } - - #[test] - fn test_hex_prefix_matches() { - let id = CommitId::from_hex("1234"); - - assert!(HexPrefix::try_from_hex("").unwrap().matches(&id)); - assert!(HexPrefix::try_from_hex("1").unwrap().matches(&id)); - assert!(HexPrefix::try_from_hex("12").unwrap().matches(&id)); - assert!(HexPrefix::try_from_hex("123").unwrap().matches(&id)); - assert!(HexPrefix::try_from_hex("1234").unwrap().matches(&id)); - assert!(!HexPrefix::try_from_hex("12345").unwrap().matches(&id)); - - assert!(!HexPrefix::try_from_hex("a").unwrap().matches(&id)); - assert!(!HexPrefix::try_from_hex("1a").unwrap().matches(&id)); - assert!(!HexPrefix::try_from_hex("12a").unwrap().matches(&id)); - assert!(!HexPrefix::try_from_hex("123a").unwrap().matches(&id)); - } -} +//! Contains helpers and macros to make it easier to work with binary Object +//! types, with a newtype wrapper [`id_type`] and the [`ObjectId`] trait. + +pub use jj_core::object_id::HexPrefix; +pub use jj_core::object_id::ObjectId; +pub use jj_core::object_id::PrefixResolution; +pub use jj_core::object_id::id_type; +pub use jj_core::object_id::impl_id_type; diff --git a/lib/src/repo_path.rs b/lib/src/repo_path.rs index 930f01c2992..096d0cab87a 100644 --- a/lib/src/repo_path.rs +++ b/lib/src/repo_path.rs @@ -14,640 +14,28 @@ #![expect(missing_docs)] -use std::borrow::Borrow; -use std::cmp::Ordering; use std::collections::HashMap; use std::fmt; use std::fmt::Debug; -use std::fmt::Formatter; use std::iter; -use std::iter::FusedIterator; -use std::ops::Deref; -use std::path::Component; -use std::path::Path; use std::path::PathBuf; use itertools::Itertools as _; -use ref_cast::RefCastCustom; -use ref_cast::ref_cast_custom; +pub use jj_core::repo_path::FsPathParseError; +pub use jj_core::repo_path::InvalidNewRepoPathError; +pub use jj_core::repo_path::InvalidRepoPathComponentError; +pub use jj_core::repo_path::InvalidRepoPathError; +pub use jj_core::repo_path::RelativePathParseError; +pub use jj_core::repo_path::RepoPath; +pub use jj_core::repo_path::RepoPathBuf; +pub use jj_core::repo_path::RepoPathComponent; +pub use jj_core::repo_path::RepoPathComponentBuf; +pub use jj_core::repo_path::RepoPathComponentsIter; use thiserror::Error; -use crate::content_hash::ContentHash; use crate::file_util; use crate::merge::Diff; -/// Owned `RepoPath` component. -#[derive(ContentHash, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct RepoPathComponentBuf { - // Don't add more fields. Eq, Hash, and Ord must be compatible with the - // borrowed RepoPathComponent type. - value: String, -} - -impl RepoPathComponentBuf { - /// Wraps `value` as `RepoPathComponentBuf`. - /// - /// Returns an error if the input `value` is empty or contains path - /// separator. - pub fn new(value: impl Into) -> Result { - let value: String = value.into(); - if is_valid_repo_path_component_str(&value) { - Ok(Self { value }) - } else { - Err(InvalidNewRepoPathError { value }) - } - } -} - -/// Borrowed `RepoPath` component. -#[derive(PartialEq, Eq, PartialOrd, Ord, Hash, RefCastCustom)] -#[repr(transparent)] -pub struct RepoPathComponent { - value: str, -} - -impl RepoPathComponent { - /// Wraps `value` as `RepoPathComponent`. - /// - /// Returns an error if the input `value` is empty or contains path - /// separator. - pub fn new(value: &str) -> Result<&Self, InvalidNewRepoPathError> { - if is_valid_repo_path_component_str(value) { - Ok(Self::new_unchecked(value)) - } else { - Err(InvalidNewRepoPathError { - value: value.to_string(), - }) - } - } - - #[ref_cast_custom] - const fn new_unchecked(value: &str) -> &Self; - - /// Returns the underlying string representation. - pub fn as_internal_str(&self) -> &str { - &self.value - } - - /// Returns a normal filesystem entry name if this path component is valid - /// as a file/directory name. - pub fn to_fs_name(&self) -> Result<&str, InvalidRepoPathComponentError> { - let mut components = Path::new(&self.value).components().fuse(); - match (components.next(), components.next()) { - // Trailing "." can be normalized by Path::components(), so compare - // component name. e.g. "foo\." (on Windows) should be rejected. - (Some(Component::Normal(name)), None) if name == &self.value => Ok(&self.value), - // e.g. ".", "..", "foo\bar" (on Windows) - _ => Err(InvalidRepoPathComponentError { - component: self.value.into(), - }), - } - } -} - -impl Debug for RepoPathComponent { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - write!(f, "{:?}", &self.value) - } -} - -impl Debug for RepoPathComponentBuf { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - ::fmt(self, f) - } -} - -impl AsRef for RepoPathComponent { - fn as_ref(&self) -> &Self { - self - } -} - -impl AsRef for RepoPathComponentBuf { - fn as_ref(&self) -> &RepoPathComponent { - self - } -} - -impl Borrow for RepoPathComponentBuf { - fn borrow(&self) -> &RepoPathComponent { - self - } -} - -impl Deref for RepoPathComponentBuf { - type Target = RepoPathComponent; - - fn deref(&self) -> &Self::Target { - RepoPathComponent::new_unchecked(&self.value) - } -} - -impl ToOwned for RepoPathComponent { - type Owned = RepoPathComponentBuf; - - fn to_owned(&self) -> Self::Owned { - let value = self.value.to_owned(); - RepoPathComponentBuf { value } - } - - fn clone_into(&self, target: &mut Self::Owned) { - self.value.clone_into(&mut target.value); - } -} - -/// Iterator over `RepoPath` components. -#[derive(Clone, Debug)] -pub struct RepoPathComponentsIter<'a> { - value: &'a str, -} - -impl<'a> RepoPathComponentsIter<'a> { - /// Returns the remaining part as repository path. - pub fn as_path(&self) -> &'a RepoPath { - RepoPath::from_internal_string_unchecked(self.value) - } -} - -impl<'a> Iterator for RepoPathComponentsIter<'a> { - type Item = &'a RepoPathComponent; - - fn next(&mut self) -> Option { - if self.value.is_empty() { - return None; - } - let (name, remainder) = self - .value - .split_once('/') - .unwrap_or_else(|| (self.value, &self.value[self.value.len()..])); - self.value = remainder; - Some(RepoPathComponent::new_unchecked(name)) - } -} - -impl DoubleEndedIterator for RepoPathComponentsIter<'_> { - fn next_back(&mut self) -> Option { - if self.value.is_empty() { - return None; - } - let (remainder, name) = self - .value - .rsplit_once('/') - .unwrap_or_else(|| (&self.value[..0], self.value)); - self.value = remainder; - Some(RepoPathComponent::new_unchecked(name)) - } -} - -impl FusedIterator for RepoPathComponentsIter<'_> {} - -/// Owned repository path. -#[derive(ContentHash, Clone, Eq, Hash, PartialEq, serde::Serialize)] -#[serde(transparent)] -pub struct RepoPathBuf { - // Don't add more fields. Eq, Hash, and Ord must be compatible with the - // borrowed RepoPath type. - value: String, -} - -/// Borrowed repository path. -#[derive(ContentHash, Eq, Hash, PartialEq, RefCastCustom, serde::Serialize)] -#[repr(transparent)] -#[serde(transparent)] -pub struct RepoPath { - value: str, -} - -impl Debug for RepoPath { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - write!(f, "{:?}", &self.value) - } -} - -impl Debug for RepoPathBuf { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - ::fmt(self, f) - } -} - -/// The `value` is not a valid repo path because it contains empty path -/// component. For example, `"/"`, `"/foo"`, `"foo/"`, `"foo//bar"` are all -/// invalid. -#[derive(Clone, Debug, Eq, Error, PartialEq)] -#[error(r#"Invalid repo path input "{value}""#)] -pub struct InvalidNewRepoPathError { - value: String, -} - -impl RepoPathBuf { - /// Creates owned repository path pointing to the root. - pub const fn root() -> Self { - Self { - value: String::new(), - } - } - - /// Creates `RepoPathBuf` from valid string representation. - pub fn from_internal_string(value: impl Into) -> Result { - let value: String = value.into(); - if is_valid_repo_path_str(&value) { - Ok(Self { value }) - } else { - Err(InvalidNewRepoPathError { value }) - } - } - - /// Converts repo-relative `Path` to `RepoPathBuf`. - /// - /// The input path should not contain redundant `.` or `..`. - pub fn from_relative_path( - relative_path: impl AsRef, - ) -> Result { - let relative_path = relative_path.as_ref(); - if relative_path == Path::new(".") { - return Ok(Self::root()); - } - - let mut components = relative_path - .components() - .map(|c| match c { - Component::Normal(name) => { - name.to_str() - .ok_or_else(|| RelativePathParseError::InvalidUtf8 { - path: relative_path.into(), - }) - } - _ => Err(RelativePathParseError::InvalidComponent { - component: c.as_os_str().to_string_lossy().into(), - path: relative_path.into(), - }), - }) - .fuse(); - let mut value = String::with_capacity(relative_path.as_os_str().len()); - if let Some(name) = components.next() { - value.push_str(name?); - } - for name in components { - value.push('/'); - value.push_str(name?); - } - Ok(Self { value }) - } - - /// Parses an `input` path into a `RepoPathBuf` relative to `base`. - /// - /// The `cwd` and `base` paths are supposed to be absolute and normalized in - /// the same manner. The `input` path may be either relative to `cwd` or - /// absolute. - pub fn parse_fs_path( - cwd: &Path, - base: &Path, - input: impl AsRef, - ) -> Result { - let input = input.as_ref(); - let abs_input_path = file_util::normalize_path(&cwd.join(input)); - let repo_relative_path = file_util::relative_path(base, &abs_input_path); - Self::from_relative_path(repo_relative_path).map_err(|source| FsPathParseError { - base: file_util::relative_path(cwd, base).into(), - input: input.into(), - source, - }) - } - - /// Consumes this and returns the underlying string representation. - pub fn into_internal_string(self) -> String { - self.value - } -} - -impl RepoPath { - /// Returns repository path pointing to the root. - pub const fn root() -> &'static Self { - Self::from_internal_string_unchecked("") - } - - /// Wraps valid string representation as `RepoPath`. - /// - /// Returns an error if the input `value` contains empty path component. For - /// example, `"/"`, `"/foo"`, `"foo/"`, `"foo//bar"` are all invalid. - pub fn from_internal_string(value: &str) -> Result<&Self, InvalidNewRepoPathError> { - if is_valid_repo_path_str(value) { - Ok(Self::from_internal_string_unchecked(value)) - } else { - Err(InvalidNewRepoPathError { - value: value.to_owned(), - }) - } - } - - #[ref_cast_custom] - const fn from_internal_string_unchecked(value: &str) -> &Self; - - /// The full string form used internally, not for presenting to users (where - /// we may want to use the platform's separator). This format includes a - /// trailing slash, unless this path represents the root directory. That - /// way it can be concatenated with a basename and produce a valid path. - pub fn to_internal_dir_string(&self) -> String { - if self.value.is_empty() { - String::new() - } else { - [&self.value, "/"].concat() - } - } - - /// The full string form used internally, not for presenting to users (where - /// we may want to use the platform's separator). - pub fn as_internal_file_string(&self) -> &str { - &self.value - } - - /// Converts repository path to filesystem path relative to the `base`. - /// - /// The returned path should never contain `..`, `C:` (on Windows), etc. - /// However, it may contain reserved working-copy directories such as `.jj`. - pub fn to_fs_path(&self, base: &Path) -> Result { - let mut result = PathBuf::with_capacity(base.as_os_str().len() + self.value.len() + 1); - result.push(base); - for c in self.components() { - result.push(c.to_fs_name().map_err(|err| err.with_path(self))?); - } - if result.as_os_str().is_empty() { - result.push("."); - } - Ok(result) - } - - /// Converts repository path to filesystem path relative to the `base`, - /// without checking invalid path components. - /// - /// The returned path may point outside of the `base` directory. Use this - /// function only for displaying or testing purposes. - pub fn to_fs_path_unchecked(&self, base: &Path) -> PathBuf { - let mut result = PathBuf::with_capacity(base.as_os_str().len() + self.value.len() + 1); - result.push(base); - result.extend(self.components().map(RepoPathComponent::as_internal_str)); - if result.as_os_str().is_empty() { - result.push("."); - } - result - } - - pub fn is_root(&self) -> bool { - self.value.is_empty() - } - - /// Returns true if the `base` is a prefix of this path. - pub fn starts_with(&self, base: &Self) -> bool { - self.strip_prefix(base).is_some() - } - - /// Returns the remaining path with the `base` path removed. - pub fn strip_prefix(&self, base: &Self) -> Option<&Self> { - if base.value.is_empty() { - Some(self) - } else { - let tail = self.value.strip_prefix(&base.value)?; - if tail.is_empty() { - Some(Self::from_internal_string_unchecked(tail)) - } else { - tail.strip_prefix('/') - .map(Self::from_internal_string_unchecked) - } - } - } - - /// Returns the parent path without the base name component. - pub fn parent(&self) -> Option<&Self> { - self.split().map(|(parent, _)| parent) - } - - /// Splits this into the parent path and base name component. - pub fn split(&self) -> Option<(&Self, &RepoPathComponent)> { - let mut components = self.components(); - let basename = components.next_back()?; - Some((components.as_path(), basename)) - } - - pub fn components(&self) -> RepoPathComponentsIter<'_> { - RepoPathComponentsIter { value: &self.value } - } - - pub fn ancestors(&self) -> impl Iterator { - std::iter::successors(Some(self), |path| path.parent()) - } - - pub fn join(&self, entry: &RepoPathComponent) -> RepoPathBuf { - let value = if self.value.is_empty() { - entry.as_internal_str().to_owned() - } else { - [&self.value, "/", entry.as_internal_str()].concat() - }; - RepoPathBuf { value } - } - - /// Splits this path at its common prefix with `other`. - /// - /// # Returns - /// - /// Returns the `(common_prefix, self_remainder)`. - /// - /// All paths will at least have `RepoPath::root()` as a common prefix, - /// therefore even if `self` and `other` have no matching parent component - /// this function will always return at least `(RepoPath::root(), self)`. - /// - /// - /// # Examples - /// - /// ``` - /// use jj_lib::repo_path::RepoPath; - /// - /// let bing_path = RepoPath::from_internal_string("foo/bar/bing").unwrap(); - /// - /// let baz_path = RepoPath::from_internal_string("foo/bar/baz").unwrap(); - /// - /// let foo_bar_path = RepoPath::from_internal_string("foo/bar").unwrap(); - /// - /// assert_eq!( - /// bing_path.split_common_prefix(&baz_path), - /// (foo_bar_path, RepoPath::from_internal_string("bing").unwrap()) - /// ); - /// - /// let unrelated_path = RepoPath::from_internal_string("no/common/prefix").unwrap(); - /// assert_eq!( - /// baz_path.split_common_prefix(&unrelated_path), - /// (RepoPath::root(), baz_path) - /// ); - /// ``` - pub fn split_common_prefix(&self, other: &Self) -> (&Self, &Self) { - // Obtain the common prefix between these paths - let mut prefix_len = 0; - - let common_components = self - .components() - .zip(other.components()) - .take_while(|(prev_comp, this_comp)| prev_comp == this_comp); - - for (self_comp, _other_comp) in common_components { - if prefix_len > 0 { - // + 1 for all paths to take their separators into account. - // We skip the first one since there are ComponentCount - 1 separators in a - // path. - prefix_len += 1; - } - - prefix_len += self_comp.value.len(); - } - - if prefix_len == 0 { - // No common prefix except root - return (Self::root(), self); - } - - if prefix_len == self.value.len() { - return (self, Self::root()); - } - - let common_prefix = Self::from_internal_string_unchecked(&self.value[..prefix_len]); - let remainder = Self::from_internal_string_unchecked(&self.value[prefix_len + 1..]); - - (common_prefix, remainder) - } -} - -impl AsRef for RepoPath { - fn as_ref(&self) -> &Self { - self - } -} - -impl AsRef for RepoPathBuf { - fn as_ref(&self) -> &RepoPath { - self - } -} - -impl Borrow for RepoPathBuf { - fn borrow(&self) -> &RepoPath { - self - } -} - -impl Deref for RepoPathBuf { - type Target = RepoPath; - - fn deref(&self) -> &Self::Target { - RepoPath::from_internal_string_unchecked(&self.value) - } -} - -impl ToOwned for RepoPath { - type Owned = RepoPathBuf; - - fn to_owned(&self) -> Self::Owned { - let value = self.value.to_owned(); - RepoPathBuf { value } - } - - fn clone_into(&self, target: &mut Self::Owned) { - self.value.clone_into(&mut target.value); - } -} - -impl Ord for RepoPath { - fn cmp(&self, other: &Self) -> Ordering { - // If there were leading/trailing slash, components-based Ord would - // disagree with str-based Eq. - debug_assert!(is_valid_repo_path_str(&self.value)); - self.components().cmp(other.components()) - } -} - -impl Ord for RepoPathBuf { - fn cmp(&self, other: &Self) -> Ordering { - ::cmp(self, other) - } -} - -impl PartialOrd for RepoPath { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} - -impl PartialOrd for RepoPathBuf { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} - -impl> Extend

for RepoPathBuf { - fn extend>(&mut self, iter: T) { - for component in iter { - if !self.value.is_empty() { - self.value.push('/'); - } - self.value.push_str(component.as_ref().as_internal_str()); - } - } -} - -/// `RepoPath` contained invalid file/directory component such as `..`. -#[derive(Clone, Debug, Eq, Error, PartialEq)] -#[error(r#"Invalid repository path "{}""#, path.as_internal_file_string())] -pub struct InvalidRepoPathError { - /// Path containing an error. - pub path: RepoPathBuf, - /// Source error. - pub source: InvalidRepoPathComponentError, -} - -/// `RepoPath` component was invalid. (e.g. `..`) -#[derive(Clone, Debug, Eq, Error, PartialEq)] -#[error(r#"Invalid path component "{component}""#)] -pub struct InvalidRepoPathComponentError { - pub component: Box, -} - -impl InvalidRepoPathComponentError { - /// Attaches the `path` that caused the error. - pub fn with_path(self, path: &RepoPath) -> InvalidRepoPathError { - InvalidRepoPathError { - path: path.to_owned(), - source: self, - } - } -} - -#[derive(Clone, Debug, Eq, Error, PartialEq)] -pub enum RelativePathParseError { - #[error(r#"Invalid component "{component}" in repo-relative path "{path}""#)] - InvalidComponent { - component: Box, - path: Box, - }, - #[error(r#"Not valid UTF-8 path "{path}""#)] - InvalidUtf8 { path: Box }, -} - -#[derive(Clone, Debug, Eq, Error, PartialEq)] -#[error(r#"Path "{input}" is not in the repo "{base}""#)] -pub struct FsPathParseError { - /// Repository or workspace root path relative to the `cwd`. - pub base: Box, - /// Input path without normalization. - pub input: Box, - /// Source error. - pub source: RelativePathParseError, -} - -fn is_valid_repo_path_component_str(value: &str) -> bool { - !value.is_empty() && !value.contains('/') -} - -fn is_valid_repo_path_str(value: &str) -> bool { - !value.starts_with('/') && !value.ends_with('/') && !value.contains("//") -} - /// An error from `RepoPathUiConverter::parse_file_path`. #[derive(Debug, Error)] pub enum UiPathParseError { @@ -839,485 +227,13 @@ impl Debug for RepoPathTree { #[cfg(test)] mod tests { - use std::panic; - - use assert_matches::assert_matches; - use itertools::Itertools as _; use super::*; - use crate::tests::new_temp_dir; fn repo_path(value: &str) -> &RepoPath { RepoPath::from_internal_string(value).unwrap() } - fn repo_path_component(value: &str) -> &RepoPathComponent { - RepoPathComponent::new(value).unwrap() - } - - #[test] - fn test_is_root() { - assert!(RepoPath::root().is_root()); - assert!(repo_path("").is_root()); - assert!(!repo_path("foo").is_root()); - } - - #[test] - fn test_from_internal_string() { - let repo_path_buf = |value: &str| RepoPathBuf::from_internal_string(value).unwrap(); - assert_eq!(repo_path_buf(""), RepoPathBuf::root()); - assert!(panic::catch_unwind(|| repo_path_buf("/")).is_err()); - assert!(panic::catch_unwind(|| repo_path_buf("/x")).is_err()); - assert!(panic::catch_unwind(|| repo_path_buf("x/")).is_err()); - assert!(panic::catch_unwind(|| repo_path_buf("x//y")).is_err()); - - assert_eq!(repo_path(""), RepoPath::root()); - assert!(panic::catch_unwind(|| repo_path("/")).is_err()); - assert!(panic::catch_unwind(|| repo_path("/x")).is_err()); - assert!(panic::catch_unwind(|| repo_path("x/")).is_err()); - assert!(panic::catch_unwind(|| repo_path("x//y")).is_err()); - } - - #[test] - fn test_as_internal_file_string() { - assert_eq!(RepoPath::root().as_internal_file_string(), ""); - assert_eq!(repo_path("dir").as_internal_file_string(), "dir"); - assert_eq!(repo_path("dir/file").as_internal_file_string(), "dir/file"); - } - - #[test] - fn test_to_internal_dir_string() { - assert_eq!(RepoPath::root().to_internal_dir_string(), ""); - assert_eq!(repo_path("dir").to_internal_dir_string(), "dir/"); - assert_eq!(repo_path("dir/file").to_internal_dir_string(), "dir/file/"); - } - - #[test] - fn test_starts_with() { - assert!(repo_path("").starts_with(repo_path(""))); - assert!(repo_path("x").starts_with(repo_path(""))); - assert!(!repo_path("").starts_with(repo_path("x"))); - - assert!(repo_path("x").starts_with(repo_path("x"))); - assert!(repo_path("x/y").starts_with(repo_path("x"))); - assert!(!repo_path("xy").starts_with(repo_path("x"))); - assert!(!repo_path("x/y").starts_with(repo_path("y"))); - - assert!(repo_path("x/y").starts_with(repo_path("x/y"))); - assert!(repo_path("x/y/z").starts_with(repo_path("x/y"))); - assert!(!repo_path("x/yz").starts_with(repo_path("x/y"))); - assert!(!repo_path("x").starts_with(repo_path("x/y"))); - assert!(!repo_path("xy").starts_with(repo_path("x/y"))); - } - - #[test] - fn test_strip_prefix() { - assert_eq!( - repo_path("").strip_prefix(repo_path("")), - Some(repo_path("")) - ); - assert_eq!( - repo_path("x").strip_prefix(repo_path("")), - Some(repo_path("x")) - ); - assert_eq!(repo_path("").strip_prefix(repo_path("x")), None); - - assert_eq!( - repo_path("x").strip_prefix(repo_path("x")), - Some(repo_path("")) - ); - assert_eq!( - repo_path("x/y").strip_prefix(repo_path("x")), - Some(repo_path("y")) - ); - assert_eq!(repo_path("xy").strip_prefix(repo_path("x")), None); - assert_eq!(repo_path("x/y").strip_prefix(repo_path("y")), None); - - assert_eq!( - repo_path("x/y").strip_prefix(repo_path("x/y")), - Some(repo_path("")) - ); - assert_eq!( - repo_path("x/y/z").strip_prefix(repo_path("x/y")), - Some(repo_path("z")) - ); - assert_eq!(repo_path("x/yz").strip_prefix(repo_path("x/y")), None); - assert_eq!(repo_path("x").strip_prefix(repo_path("x/y")), None); - assert_eq!(repo_path("xy").strip_prefix(repo_path("x/y")), None); - } - - #[test] - fn test_order() { - assert!(RepoPath::root() < repo_path("dir")); - assert!(repo_path("dir") < repo_path("dirx")); - // '#' < '/', but ["dir", "sub"] < ["dir#"] - assert!(repo_path("dir") < repo_path("dir#")); - assert!(repo_path("dir") < repo_path("dir/sub")); - assert!(repo_path("dir/sub") < repo_path("dir#")); - - assert!(repo_path("abc") < repo_path("dir/file")); - assert!(repo_path("dir") < repo_path("dir/file")); - assert!(repo_path("dis") > repo_path("dir/file")); - assert!(repo_path("xyz") > repo_path("dir/file")); - assert!(repo_path("dir1/xyz") < repo_path("dir2/abc")); - } - - #[test] - fn test_join() { - let root = RepoPath::root(); - let dir = root.join(repo_path_component("dir")); - assert_eq!(dir.as_ref(), repo_path("dir")); - let subdir = dir.join(repo_path_component("subdir")); - assert_eq!(subdir.as_ref(), repo_path("dir/subdir")); - assert_eq!( - subdir.join(repo_path_component("file")).as_ref(), - repo_path("dir/subdir/file") - ); - } - - #[test] - fn test_extend() { - let mut path = RepoPathBuf::root(); - path.extend(std::iter::empty::()); - assert_eq!(path.as_ref(), RepoPath::root()); - path.extend([repo_path_component("dir")]); - assert_eq!(path.as_ref(), repo_path("dir")); - path.extend(std::iter::repeat_n(repo_path_component("subdir"), 3)); - assert_eq!(path.as_ref(), repo_path("dir/subdir/subdir/subdir")); - path.extend(std::iter::empty::()); - assert_eq!(path.as_ref(), repo_path("dir/subdir/subdir/subdir")); - } - - #[test] - fn test_parent() { - let root = RepoPath::root(); - let dir_component = repo_path_component("dir"); - let subdir_component = repo_path_component("subdir"); - - let dir = root.join(dir_component); - let subdir = dir.join(subdir_component); - - assert_eq!(root.parent(), None); - assert_eq!(dir.parent(), Some(root)); - assert_eq!(subdir.parent(), Some(dir.as_ref())); - } - - #[test] - fn test_split() { - let root = RepoPath::root(); - let dir_component = repo_path_component("dir"); - let file_component = repo_path_component("file"); - - let dir = root.join(dir_component); - let file = dir.join(file_component); - - assert_eq!(root.split(), None); - assert_eq!(dir.split(), Some((root, dir_component))); - assert_eq!(file.split(), Some((dir.as_ref(), file_component))); - } - - #[test] - fn test_components() { - assert!(RepoPath::root().components().next().is_none()); - assert_eq!( - repo_path("dir").components().collect_vec(), - vec![repo_path_component("dir")] - ); - assert_eq!( - repo_path("dir/subdir").components().collect_vec(), - vec![repo_path_component("dir"), repo_path_component("subdir")] - ); - - // Iterates from back - assert!(RepoPath::root().components().next_back().is_none()); - assert_eq!( - repo_path("dir").components().rev().collect_vec(), - vec![repo_path_component("dir")] - ); - assert_eq!( - repo_path("dir/subdir").components().rev().collect_vec(), - vec![repo_path_component("subdir"), repo_path_component("dir")] - ); - } - - #[test] - fn test_ancestors() { - assert_eq!( - RepoPath::root().ancestors().collect_vec(), - vec![RepoPath::root()] - ); - assert_eq!( - repo_path("dir").ancestors().collect_vec(), - vec![repo_path("dir"), RepoPath::root()] - ); - assert_eq!( - repo_path("dir/subdir").ancestors().collect_vec(), - vec![repo_path("dir/subdir"), repo_path("dir"), RepoPath::root()] - ); - } - - #[test] - fn test_to_fs_path() { - assert_eq!( - repo_path("").to_fs_path(Path::new("base/dir")).unwrap(), - Path::new("base/dir") - ); - assert_eq!( - repo_path("").to_fs_path(Path::new("")).unwrap(), - Path::new(".") - ); - assert_eq!( - repo_path("file").to_fs_path(Path::new("base/dir")).unwrap(), - Path::new("base/dir/file") - ); - assert_eq!( - repo_path("some/deep/dir/file") - .to_fs_path(Path::new("base/dir")) - .unwrap(), - Path::new("base/dir/some/deep/dir/file") - ); - assert_eq!( - repo_path("dir/file").to_fs_path(Path::new("")).unwrap(), - Path::new("dir/file") - ); - - // Current/parent dir component - assert!(repo_path(".").to_fs_path(Path::new("base")).is_err()); - assert!(repo_path("..").to_fs_path(Path::new("base")).is_err()); - assert!( - repo_path("dir/../file") - .to_fs_path(Path::new("base")) - .is_err() - ); - assert!(repo_path("./file").to_fs_path(Path::new("base")).is_err()); - assert!(repo_path("file/.").to_fs_path(Path::new("base")).is_err()); - assert!(repo_path("../file").to_fs_path(Path::new("base")).is_err()); - assert!(repo_path("file/..").to_fs_path(Path::new("base")).is_err()); - - // Empty component (which is invalid as a repo path) - assert!( - RepoPath::from_internal_string_unchecked("/") - .to_fs_path(Path::new("base")) - .is_err() - ); - assert_eq!( - // Iterator omits empty component after "/", which is fine so long - // as the returned path doesn't escape. - RepoPath::from_internal_string_unchecked("a/") - .to_fs_path(Path::new("base")) - .unwrap(), - Path::new("base/a") - ); - assert!( - RepoPath::from_internal_string_unchecked("/b") - .to_fs_path(Path::new("base")) - .is_err() - ); - assert!( - RepoPath::from_internal_string_unchecked("a//b") - .to_fs_path(Path::new("base")) - .is_err() - ); - - // Component containing slash (simulating Windows path separator) - assert!( - RepoPathComponent::new_unchecked("wind/ows") - .to_fs_name() - .is_err() - ); - assert!( - RepoPathComponent::new_unchecked("./file") - .to_fs_name() - .is_err() - ); - assert!( - RepoPathComponent::new_unchecked("file/.") - .to_fs_name() - .is_err() - ); - assert!(RepoPathComponent::new_unchecked("/").to_fs_name().is_err()); - - // Windows path separator and drive letter - if cfg!(windows) { - assert!( - repo_path(r#"wind\ows"#) - .to_fs_path(Path::new("base")) - .is_err() - ); - assert!( - repo_path(r#".\file"#) - .to_fs_path(Path::new("base")) - .is_err() - ); - assert!( - repo_path(r#"file\."#) - .to_fs_path(Path::new("base")) - .is_err() - ); - assert!( - repo_path(r#"c:/foo"#) - .to_fs_path(Path::new("base")) - .is_err() - ); - } - } - - #[test] - fn test_to_fs_path_unchecked() { - assert_eq!( - repo_path("").to_fs_path_unchecked(Path::new("base/dir")), - Path::new("base/dir") - ); - assert_eq!( - repo_path("").to_fs_path_unchecked(Path::new("")), - Path::new(".") - ); - assert_eq!( - repo_path("file").to_fs_path_unchecked(Path::new("base/dir")), - Path::new("base/dir/file") - ); - assert_eq!( - repo_path("some/deep/dir/file").to_fs_path_unchecked(Path::new("base/dir")), - Path::new("base/dir/some/deep/dir/file") - ); - assert_eq!( - repo_path("dir/file").to_fs_path_unchecked(Path::new("")), - Path::new("dir/file") - ); - } - - #[test] - fn parse_fs_path_wc_in_cwd() { - let temp_dir = new_temp_dir(); - let cwd_path = temp_dir.path().join("repo"); - let wc_path = &cwd_path; - - assert_eq!( - RepoPathBuf::parse_fs_path(&cwd_path, wc_path, "").as_deref(), - Ok(RepoPath::root()) - ); - assert_eq!( - RepoPathBuf::parse_fs_path(&cwd_path, wc_path, ".").as_deref(), - Ok(RepoPath::root()) - ); - assert_eq!( - RepoPathBuf::parse_fs_path(&cwd_path, wc_path, "file").as_deref(), - Ok(repo_path("file")) - ); - // Both slash and the platform's separator are allowed - assert_eq!( - RepoPathBuf::parse_fs_path( - &cwd_path, - wc_path, - format!("dir{}file", std::path::MAIN_SEPARATOR) - ) - .as_deref(), - Ok(repo_path("dir/file")) - ); - assert_eq!( - RepoPathBuf::parse_fs_path(&cwd_path, wc_path, "dir/file").as_deref(), - Ok(repo_path("dir/file")) - ); - assert_matches!( - RepoPathBuf::parse_fs_path(&cwd_path, wc_path, ".."), - Err(FsPathParseError { - source: RelativePathParseError::InvalidComponent { .. }, - .. - }) - ); - assert_eq!( - RepoPathBuf::parse_fs_path(&cwd_path, &cwd_path, "../repo").as_deref(), - Ok(RepoPath::root()) - ); - assert_eq!( - RepoPathBuf::parse_fs_path(&cwd_path, &cwd_path, "../repo/file").as_deref(), - Ok(repo_path("file")) - ); - // Input may be absolute path with ".." - assert_eq!( - RepoPathBuf::parse_fs_path( - &cwd_path, - &cwd_path, - cwd_path.join("../repo").to_str().unwrap() - ) - .as_deref(), - Ok(RepoPath::root()) - ); - } - - #[test] - fn parse_fs_path_wc_in_cwd_parent() { - let temp_dir = new_temp_dir(); - let cwd_path = temp_dir.path().join("dir"); - let wc_path = cwd_path.parent().unwrap().to_path_buf(); - - assert_eq!( - RepoPathBuf::parse_fs_path(&cwd_path, &wc_path, "").as_deref(), - Ok(repo_path("dir")) - ); - assert_eq!( - RepoPathBuf::parse_fs_path(&cwd_path, &wc_path, ".").as_deref(), - Ok(repo_path("dir")) - ); - assert_eq!( - RepoPathBuf::parse_fs_path(&cwd_path, &wc_path, "file").as_deref(), - Ok(repo_path("dir/file")) - ); - assert_eq!( - RepoPathBuf::parse_fs_path(&cwd_path, &wc_path, "subdir/file").as_deref(), - Ok(repo_path("dir/subdir/file")) - ); - assert_eq!( - RepoPathBuf::parse_fs_path(&cwd_path, &wc_path, "..").as_deref(), - Ok(RepoPath::root()) - ); - assert_matches!( - RepoPathBuf::parse_fs_path(&cwd_path, &wc_path, "../.."), - Err(FsPathParseError { - source: RelativePathParseError::InvalidComponent { .. }, - .. - }) - ); - assert_eq!( - RepoPathBuf::parse_fs_path(&cwd_path, &wc_path, "../other-dir/file").as_deref(), - Ok(repo_path("other-dir/file")) - ); - } - - #[test] - fn parse_fs_path_wc_in_cwd_child() { - let temp_dir = new_temp_dir(); - let cwd_path = temp_dir.path().join("cwd"); - let wc_path = cwd_path.join("repo"); - - assert_matches!( - RepoPathBuf::parse_fs_path(&cwd_path, &wc_path, ""), - Err(FsPathParseError { - source: RelativePathParseError::InvalidComponent { .. }, - .. - }) - ); - assert_matches!( - RepoPathBuf::parse_fs_path(&cwd_path, &wc_path, "not-repo"), - Err(FsPathParseError { - source: RelativePathParseError::InvalidComponent { .. }, - .. - }) - ); - assert_eq!( - RepoPathBuf::parse_fs_path(&cwd_path, &wc_path, "repo").as_deref(), - Ok(RepoPath::root()) - ); - assert_eq!( - RepoPathBuf::parse_fs_path(&cwd_path, &wc_path, "repo/file").as_deref(), - Ok(repo_path("file")) - ); - assert_eq!( - RepoPathBuf::parse_fs_path(&cwd_path, &wc_path, "repo/dir/file").as_deref(), - Ok(repo_path("dir/file")) - ); - } - #[test] fn test_format_copied_path() { let ui = RepoPathUiConverter::Fs { @@ -1359,47 +275,4 @@ mod tests { "x/something/{ => something}/1to2.txt" ); } - - #[test] - fn test_split_common_prefix() { - assert_eq!( - repo_path("foo/bar").split_common_prefix(repo_path("foo/bar/baz")), - (repo_path("foo/bar"), repo_path("")) - ); - - assert_eq!( - repo_path("foo/bar/baz").split_common_prefix(repo_path("foo/bar")), - (repo_path("foo/bar"), repo_path("baz")) - ); - - assert_eq!( - repo_path("foo/bar/bing").split_common_prefix(repo_path("foo/bar/baz")), - (repo_path("foo/bar"), repo_path("bing")) - ); - - assert_eq!( - repo_path("no/common/prefix").split_common_prefix(repo_path("foo/bar/baz")), - (RepoPath::root(), repo_path("no/common/prefix")) - ); - - assert_eq!( - repo_path("same/path").split_common_prefix(repo_path("same/path")), - (repo_path("same/path"), RepoPath::root()) - ); - - assert_eq!( - RepoPath::root().split_common_prefix(repo_path("foo")), - (RepoPath::root(), RepoPath::root()) - ); - - assert_eq!( - RepoPath::root().split_common_prefix(RepoPath::root()), - (RepoPath::root(), RepoPath::root()) - ); - - assert_eq!( - repo_path("foo/bar").split_common_prefix(RepoPath::root()), - (RepoPath::root(), repo_path("foo/bar")) - ); - } } diff --git a/lib/src/revset.rs b/lib/src/revset.rs index dbdd2cf9ad9..2b1f5c03dc8 100644 --- a/lib/src/revset.rs +++ b/lib/src/revset.rs @@ -29,6 +29,9 @@ use futures::StreamExt as _; use futures::future::LocalBoxFuture; use futures::stream::LocalBoxStream; use itertools::Itertools as _; +pub use jj_core::revset::format_remote_symbol; +pub use jj_core::revset::format_string; +pub use jj_core::revset::format_symbol; use pollster::FutureExt as _; use thiserror::Error; @@ -3578,31 +3581,6 @@ pub struct RevsetWorkspaceContext<'a> { pub workspace_name: &'a WorkspaceName, } -/// Formats a string as symbol by quoting and escaping it if necessary. -/// -/// Note that symbols may be substituted to user aliases. Use -/// [`format_string()`] to ensure that the provided string is resolved as a -/// tag/bookmark name, commit/change ID prefix, etc. -pub fn format_symbol(literal: &str) -> String { - if revset_parser::is_identifier(literal) { - literal.to_string() - } else { - format_string(literal) - } -} - -/// Formats a string by quoting and escaping it. -pub fn format_string(literal: &str) -> String { - format!(r#""{}""#, dsl_util::escape_string(literal)) -} - -/// Formats a `name@remote` symbol, applies quoting and escaping if necessary. -pub fn format_remote_symbol(name: &str, remote: &str) -> String { - let name = format_symbol(name); - let remote = format_symbol(remote); - format!("{name}@{remote}") -} - #[cfg(test)] #[rustversion::attr( since(1.89), diff --git a/lib/src/revset_parser.rs b/lib/src/revset_parser.rs index 163f6f2b585..ec0b29034c3 100644 --- a/lib/src/revset_parser.rs +++ b/lib/src/revset_parser.rs @@ -12,1948 +12,26 @@ // See the License for the specific language governing permissions and // limitations under the License. -#![expect(missing_docs)] - -use std::collections::HashSet; -use std::error; -use std::mem; -use std::str::FromStr; -use std::sync::LazyLock; - -use itertools::Itertools as _; -use pest::Parser as _; -use pest::iterators::Pair; -use pest::pratt_parser::Assoc; -use pest::pratt_parser::Op; -use pest::pratt_parser::PrattParser; -use pest_derive::Parser; -use thiserror::Error; - -use crate::dsl_util; -use crate::dsl_util::AliasDeclaration; -use crate::dsl_util::AliasDeclarationParser; -use crate::dsl_util::AliasDefinitionParser; -use crate::dsl_util::AliasExpandError; -use crate::dsl_util::AliasExpandableExpression; -use crate::dsl_util::AliasId; -use crate::dsl_util::AliasesMap; -use crate::dsl_util::Diagnostics; -use crate::dsl_util::ExpressionFolder; -use crate::dsl_util::FoldableExpression; -use crate::dsl_util::FunctionCallParser; -use crate::dsl_util::InvalidArguments; -use crate::dsl_util::StringLiteralParser; -use crate::dsl_util::collect_similar; -use crate::ref_name::RefNameBuf; -use crate::ref_name::RemoteNameBuf; -use crate::ref_name::RemoteRefSymbolBuf; - -#[derive(Parser)] -#[grammar = "revset.pest"] -struct RevsetParser; - -const STRING_LITERAL_PARSER: StringLiteralParser = StringLiteralParser { - content_rule: Rule::string_content, - escape_rule: Rule::string_escape, -}; -const FUNCTION_CALL_PARSER: FunctionCallParser = FunctionCallParser { - function_name_rule: Rule::function_name, - function_arguments_rule: Rule::function_arguments, - keyword_argument_rule: Rule::keyword_argument, - argument_name_rule: Rule::strict_identifier, - argument_value_rule: Rule::expression, -}; - -impl Rule { - /// Whether this is a placeholder rule for compatibility with the other - /// systems. - fn is_compat(&self) -> bool { - matches!( - self, - Self::compat_parents_op - | Self::compat_dag_range_op - | Self::compat_dag_range_pre_op - | Self::compat_dag_range_post_op - | Self::compat_add_op - | Self::compat_sub_op - ) - } - - fn to_symbol(self) -> Option<&'static str> { - match self { - Self::EOI => None, - Self::whitespace => None, - Self::identifier_part => None, - Self::identifier => None, - Self::strict_identifier_part => None, - Self::strict_identifier => None, - Self::symbol => None, - Self::string_escape => None, - Self::string_content_char => None, - Self::string_content => None, - Self::string_literal => None, - Self::raw_string_content => None, - Self::raw_string_literal => None, - Self::at_op => Some("@"), - Self::pattern_kind_op => Some(":"), - Self::parents_op => Some("-"), - Self::children_op => Some("+"), - Self::compat_parents_op => Some("^"), - Self::dag_range_op - | Self::dag_range_pre_op - | Self::dag_range_post_op - | Self::dag_range_all_op => Some("::"), - Self::compat_dag_range_op - | Self::compat_dag_range_pre_op - | Self::compat_dag_range_post_op => Some(":"), - Self::range_op => Some(".."), - Self::range_pre_op | Self::range_post_op | Self::range_all_op => Some(".."), - Self::range_ops => None, - Self::range_pre_ops => None, - Self::range_post_ops => None, - Self::range_all_ops => None, - Self::negate_op => Some("~"), - Self::union_op => Some("|"), - Self::intersection_op => Some("&"), - Self::difference_op => Some("~"), - Self::compat_add_op => Some("+"), - Self::compat_sub_op => Some("-"), - Self::infix_op => None, - Self::function => None, - Self::function_name => None, - Self::keyword_argument => None, - Self::argument => None, - Self::function_arguments => None, - Self::formal_parameters => None, - Self::pattern => None, - Self::pattern_value_expression => None, - Self::primary => None, - Self::neighbors_expression => None, - Self::range_expression => None, - Self::expression => None, - Self::program => None, - Self::symbol_name => None, - Self::function_alias_declaration => None, - Self::pattern_alias_declaration => None, - Self::alias_declaration => None, - } - } -} - -/// Manages diagnostic messages emitted during revset parsing and function-call -/// resolution. -pub type RevsetDiagnostics = Diagnostics; - -#[derive(Debug, Error)] -#[error("{pest_error}")] -pub struct RevsetParseError { - kind: Box, - pest_error: Box>, - source: Option>, -} - -#[derive(Debug, Error, PartialEq, Eq)] -pub enum RevsetParseErrorKind { - #[error("Syntax error")] - SyntaxError, - #[error("`{op}` is not a prefix operator")] - NotPrefixOperator { - op: String, - similar_op: String, - description: String, - }, - #[error("`{op}` is not a postfix operator")] - NotPostfixOperator { - op: String, - similar_op: String, - description: String, - }, - #[error("`{op}` is not an infix operator")] - NotInfixOperator { - op: String, - similar_op: String, - description: String, - }, - #[error("Function `{name}` doesn't exist")] - NoSuchFunction { - name: String, - candidates: Vec, - }, - #[error("Function `{name}`: {message}")] - InvalidFunctionArguments { name: String, message: String }, - #[error("Cannot resolve file pattern without workspace")] - FsPathWithoutWorkspace, - #[error("Cannot resolve `@` without workspace")] - WorkingCopyWithoutWorkspace, - #[error("Redefinition of function parameter")] - RedefinedFunctionParameter, - #[error("{0}")] - Expression(String), - #[error("In alias `{0}`")] - InAliasExpansion(String), - #[error("In function parameter `{0}`")] - InParameterExpansion(String), - #[error("Alias `{0}` expanded recursively")] - RecursiveAlias(String), -} - -impl RevsetParseError { - pub(super) fn with_span(kind: RevsetParseErrorKind, span: pest::Span<'_>) -> Self { - let message = kind.to_string(); - let pest_error = Box::new(pest::error::Error::new_from_span( - pest::error::ErrorVariant::CustomError { message }, - span, - )); - Self { - kind: Box::new(kind), - pest_error, - source: None, - } - } - - pub(super) fn with_source( - mut self, - source: impl Into>, - ) -> Self { - self.source = Some(source.into()); - self - } - - /// Some other expression error. - pub fn expression(message: impl Into, span: pest::Span<'_>) -> Self { - Self::with_span(RevsetParseErrorKind::Expression(message.into()), span) - } - - /// If this is a `NoSuchFunction` error, expands the candidates list with - /// the given `other_functions`. - pub(super) fn extend_function_candidates(mut self, other_functions: I) -> Self - where - I: IntoIterator, - I::Item: AsRef, - { - if let RevsetParseErrorKind::NoSuchFunction { name, candidates } = self.kind.as_mut() { - let other_candidates = collect_similar(name, other_functions); - *candidates = itertools::merge(mem::take(candidates), other_candidates) - .dedup() - .collect(); - } - self - } - - pub fn kind(&self) -> &RevsetParseErrorKind { - &self.kind - } - - /// Original parsing error which typically occurred in an alias expression. - pub fn origin(&self) -> Option<&Self> { - self.source.as_ref().and_then(|e| e.downcast_ref()) - } -} - -impl AliasExpandError for RevsetParseError { - fn invalid_arguments(err: InvalidArguments<'_>) -> Self { - err.into() - } - - fn recursive_expansion(id: AliasId<'_>, span: pest::Span<'_>) -> Self { - Self::with_span(RevsetParseErrorKind::RecursiveAlias(id.to_string()), span) - } - - fn within_alias_expansion(self, id: AliasId<'_>, span: pest::Span<'_>) -> Self { - let kind = match id { - AliasId::Symbol(_) | AliasId::Pattern(..) | AliasId::Function(..) => { - RevsetParseErrorKind::InAliasExpansion(id.to_string()) - } - AliasId::Parameter(_) => RevsetParseErrorKind::InParameterExpansion(id.to_string()), - }; - Self::with_span(kind, span).with_source(self) - } -} - -impl From> for RevsetParseError { - fn from(err: pest::error::Error) -> Self { - Self { - kind: Box::new(RevsetParseErrorKind::SyntaxError), - pest_error: Box::new(rename_rules_in_pest_error(err)), - source: None, - } - } -} - -impl From> for RevsetParseError { - fn from(err: InvalidArguments<'_>) -> Self { - let kind = RevsetParseErrorKind::InvalidFunctionArguments { - name: err.name.to_owned(), - message: err.message, - }; - Self::with_span(kind, err.span) - } -} - -fn rename_rules_in_pest_error(mut err: pest::error::Error) -> pest::error::Error { - let pest::error::ErrorVariant::ParsingError { - positives, - negatives, - } = &mut err.variant - else { - return err; - }; - - // Remove duplicated symbols. Compat symbols are also removed from the - // (positive) suggestion. - let mut known_syms = HashSet::new(); - positives.retain(|rule| { - !rule.is_compat() && rule.to_symbol().is_none_or(|sym| known_syms.insert(sym)) - }); - let mut known_syms = HashSet::new(); - negatives.retain(|rule| rule.to_symbol().is_none_or(|sym| known_syms.insert(sym))); - err.renamed_rules(|rule| { - rule.to_symbol() - .map(|sym| format!("`{sym}`")) - .unwrap_or_else(|| format!("<{rule:?}>")) - }) -} - -#[derive(Clone, Debug, Eq, PartialEq)] -pub enum ExpressionKind<'i> { - /// Unquoted symbol. - Identifier(&'i str), - /// Quoted symbol or string. - String(String), - /// `:` where `` is usually `Identifier` or `String`. - Pattern(Box>), - /// `@` - RemoteSymbol(RemoteRefSymbolBuf), - /// `@` - AtWorkspace(String), - /// `@` - AtCurrentWorkspace, - /// `::` - DagRangeAll, - /// `..` - RangeAll, - Unary(UnaryOp, Box>), - Binary(BinaryOp, Box>, Box>), - /// `x | y | ..` - UnionAll(Vec>), - FunctionCall(Box>), - /// Identity node to preserve the span in the source text. - AliasExpanded(AliasId<'i>, Box>), -} - -impl<'i> FoldableExpression<'i> for ExpressionKind<'i> { - fn fold(self, folder: &mut F, span: pest::Span<'i>) -> Result - where - F: ExpressionFolder<'i, Self> + ?Sized, - { - match self { - Self::Identifier(name) => folder.fold_identifier(name, span), - Self::String(_) => Ok(self), - Self::Pattern(pattern) => folder.fold_pattern(pattern, span), - Self::RemoteSymbol(_) - | ExpressionKind::AtWorkspace(_) - | Self::AtCurrentWorkspace - | Self::DagRangeAll - | Self::RangeAll => Ok(self), - Self::Unary(op, arg) => { - let arg = Box::new(folder.fold_expression(*arg)?); - Ok(Self::Unary(op, arg)) - } - Self::Binary(op, lhs, rhs) => { - let lhs = Box::new(folder.fold_expression(*lhs)?); - let rhs = Box::new(folder.fold_expression(*rhs)?); - Ok(Self::Binary(op, lhs, rhs)) - } - Self::UnionAll(nodes) => { - let nodes = dsl_util::fold_expression_nodes(folder, nodes)?; - Ok(Self::UnionAll(nodes)) - } - Self::FunctionCall(function) => folder.fold_function_call(function, span), - Self::AliasExpanded(id, subst) => { - let subst = Box::new(folder.fold_expression(*subst)?); - Ok(Self::AliasExpanded(id, subst)) - } - } - } -} - -impl<'i> AliasExpandableExpression<'i> for ExpressionKind<'i> { - fn identifier(name: &'i str) -> Self { - Self::Identifier(name) - } - - fn pattern(pattern: Box>) -> Self { - Self::Pattern(pattern) - } - - fn function_call(function: Box>) -> Self { - Self::FunctionCall(function) - } - - fn alias_expanded(id: AliasId<'i>, subst: Box>) -> Self { - Self::AliasExpanded(id, subst) - } -} - -#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] -pub enum UnaryOp { - /// `~x` - Negate, - /// `::x` - DagRangePre, - /// `x::` - DagRangePost, - /// `..x` - RangePre, - /// `x..` - RangePost, - /// `x-` - Parents, - /// `x+` - Children, -} - -#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] -pub enum BinaryOp { - /// `&` - Intersection, - /// `~` - Difference, - /// `::` - DagRange, - /// `..` - Range, -} - -pub type ExpressionNode<'i> = dsl_util::ExpressionNode<'i, ExpressionKind<'i>>; -pub type FunctionCallNode<'i> = dsl_util::FunctionCallNode<'i, ExpressionKind<'i>>; -pub type PatternNode<'i> = dsl_util::PatternNode<'i, ExpressionKind<'i>>; - -fn union_nodes<'i>(lhs: ExpressionNode<'i>, rhs: ExpressionNode<'i>) -> ExpressionNode<'i> { - let span = lhs.span.start_pos().span(&rhs.span.end_pos()); - let expr = match lhs.kind { - // Flatten "x | y | z" to save recursion stack. Machine-generated query - // might have long chain of unions. - ExpressionKind::UnionAll(mut nodes) => { - nodes.push(rhs); - ExpressionKind::UnionAll(nodes) - } - _ => ExpressionKind::UnionAll(vec![lhs, rhs]), - }; - ExpressionNode::new(expr, span) -} - -/// Parses text into expression tree. No name resolution is made at this stage. -pub fn parse_program(revset_str: &str) -> Result, RevsetParseError> { - let mut pairs = RevsetParser::parse(Rule::program, revset_str)?; - let first = pairs.next().unwrap(); - assert_eq!(first.as_rule(), Rule::expression); - parse_expression_node(first) -} - -fn parse_expression_node(pair: Pair) -> Result { - fn not_prefix_op( - op: &Pair, - similar_op: impl Into, - description: impl Into, - ) -> RevsetParseError { - RevsetParseError::with_span( - RevsetParseErrorKind::NotPrefixOperator { - op: op.as_str().to_owned(), - similar_op: similar_op.into(), - description: description.into(), - }, - op.as_span(), - ) - } - - fn not_postfix_op( - op: &Pair, - similar_op: impl Into, - description: impl Into, - ) -> RevsetParseError { - RevsetParseError::with_span( - RevsetParseErrorKind::NotPostfixOperator { - op: op.as_str().to_owned(), - similar_op: similar_op.into(), - description: description.into(), - }, - op.as_span(), - ) - } - - fn not_infix_op( - op: &Pair, - similar_op: impl Into, - description: impl Into, - ) -> RevsetParseError { - RevsetParseError::with_span( - RevsetParseErrorKind::NotInfixOperator { - op: op.as_str().to_owned(), - similar_op: similar_op.into(), - description: description.into(), - }, - op.as_span(), - ) - } - - static PRATT: LazyLock> = LazyLock::new(|| { - PrattParser::new() - .op(Op::infix(Rule::union_op, Assoc::Left) - | Op::infix(Rule::compat_add_op, Assoc::Left)) - .op(Op::infix(Rule::intersection_op, Assoc::Left) - | Op::infix(Rule::difference_op, Assoc::Left) - | Op::infix(Rule::compat_sub_op, Assoc::Left)) - .op(Op::prefix(Rule::negate_op)) - // Ranges can't be nested without parentheses. Associativity doesn't matter. - .op(Op::infix(Rule::dag_range_op, Assoc::Left) - | Op::infix(Rule::compat_dag_range_op, Assoc::Left) - | Op::infix(Rule::range_op, Assoc::Left)) - .op(Op::prefix(Rule::dag_range_pre_op) - | Op::prefix(Rule::compat_dag_range_pre_op) - | Op::prefix(Rule::range_pre_op)) - .op(Op::postfix(Rule::dag_range_post_op) - | Op::postfix(Rule::compat_dag_range_post_op) - | Op::postfix(Rule::range_post_op)) - // Neighbors - .op(Op::postfix(Rule::parents_op) - | Op::postfix(Rule::children_op) - | Op::postfix(Rule::compat_parents_op)) - }); - PRATT - .map_primary(|primary| { - let expr = match primary.as_rule() { - Rule::primary => return parse_primary_node(primary), - Rule::dag_range_all_op => ExpressionKind::DagRangeAll, - Rule::range_all_op => ExpressionKind::RangeAll, - r => panic!("unexpected primary rule {r:?}"), - }; - Ok(ExpressionNode::new(expr, primary.as_span())) - }) - .map_prefix(|op, rhs| { - let op_kind = match op.as_rule() { - Rule::negate_op => UnaryOp::Negate, - Rule::dag_range_pre_op => UnaryOp::DagRangePre, - Rule::compat_dag_range_pre_op => Err(not_prefix_op(&op, "::", "ancestors"))?, - Rule::range_pre_op => UnaryOp::RangePre, - r => panic!("unexpected prefix operator rule {r:?}"), - }; - let rhs = Box::new(rhs?); - let span = op.as_span().start_pos().span(&rhs.span.end_pos()); - let expr = ExpressionKind::Unary(op_kind, rhs); - Ok(ExpressionNode::new(expr, span)) - }) - .map_postfix(|lhs, op| { - let op_kind = match op.as_rule() { - Rule::dag_range_post_op => UnaryOp::DagRangePost, - Rule::compat_dag_range_post_op => Err(not_postfix_op(&op, "::", "descendants"))?, - Rule::range_post_op => UnaryOp::RangePost, - Rule::parents_op => UnaryOp::Parents, - Rule::children_op => UnaryOp::Children, - Rule::compat_parents_op => Err(not_postfix_op(&op, "-", "parents"))?, - r => panic!("unexpected postfix operator rule {r:?}"), - }; - let lhs = Box::new(lhs?); - let span = lhs.span.start_pos().span(&op.as_span().end_pos()); - let expr = ExpressionKind::Unary(op_kind, lhs); - Ok(ExpressionNode::new(expr, span)) - }) - .map_infix(|lhs, op, rhs| { - let op_kind = match op.as_rule() { - Rule::union_op => return Ok(union_nodes(lhs?, rhs?)), - Rule::compat_add_op => Err(not_infix_op(&op, "|", "union"))?, - Rule::intersection_op => BinaryOp::Intersection, - Rule::difference_op => BinaryOp::Difference, - Rule::compat_sub_op => Err(not_infix_op(&op, "~", "difference"))?, - Rule::dag_range_op => BinaryOp::DagRange, - Rule::compat_dag_range_op => Err(not_infix_op(&op, "::", "DAG range"))?, - Rule::range_op => BinaryOp::Range, - r => panic!("unexpected infix operator rule {r:?}"), - }; - let lhs = Box::new(lhs?); - let rhs = Box::new(rhs?); - let span = lhs.span.start_pos().span(&rhs.span.end_pos()); - let expr = ExpressionKind::Binary(op_kind, lhs, rhs); - Ok(ExpressionNode::new(expr, span)) - }) - .parse(pair.into_inner()) -} - -fn parse_primary_node(pair: Pair) -> Result { - let span = pair.as_span(); - let mut pairs = pair.into_inner(); - let first = pairs.next().unwrap(); - let expr = match first.as_rule() { - // Ignore inner span to preserve parenthesized expression as such. - Rule::expression => parse_expression_node(first)?.kind, - Rule::function => { - let function = Box::new(FUNCTION_CALL_PARSER.parse( - first, - |pair| Ok(pair.as_str()), - |pair| parse_expression_node(pair), - )?); - ExpressionKind::FunctionCall(function) - } - Rule::pattern => { - let [lhs, op, rhs] = first.into_inner().collect_array().unwrap(); - assert_eq!(lhs.as_rule(), Rule::strict_identifier); - assert_eq!(op.as_rule(), Rule::pattern_kind_op); - assert_eq!(rhs.as_rule(), Rule::pattern_value_expression); - let pattern = Box::new(PatternNode { - name: lhs.as_str(), - name_span: lhs.as_span(), - value: parse_expression_node(rhs)?, - }); - ExpressionKind::Pattern(pattern) - } - // Identifier without "@" may be substituted by aliases. Primary expression including "@" - // is considered an indecomposable unit, and no alias substitution would be made. - Rule::identifier if pairs.peek().is_none() => ExpressionKind::Identifier(first.as_str()), - Rule::identifier | Rule::string_literal | Rule::raw_string_literal => { - let name = parse_as_string_literal(first); - match pairs.next() { - None => ExpressionKind::String(name), - Some(op) => { - assert_eq!(op.as_rule(), Rule::at_op); - match pairs.next() { - // postfix "@" - None => ExpressionKind::AtWorkspace(name), - // infix "@" - Some(second) => { - let name: RefNameBuf = name.into(); - let remote: RemoteNameBuf = parse_as_string_literal(second).into(); - ExpressionKind::RemoteSymbol(RemoteRefSymbolBuf { name, remote }) - } - } - } - } - } - // nullary "@" - Rule::at_op => ExpressionKind::AtCurrentWorkspace, - r => panic!("unexpected revset parse rule: {r:?}"), - }; - Ok(ExpressionNode::new(expr, span)) -} - -/// Parses part of compound symbol to string. -fn parse_as_string_literal(pair: Pair) -> String { - match pair.as_rule() { - Rule::identifier => pair.as_str().to_owned(), - Rule::string_literal => STRING_LITERAL_PARSER.parse(pair.into_inner()), - Rule::raw_string_literal => { - let [content] = pair.into_inner().collect_array().unwrap(); - assert_eq!(content.as_rule(), Rule::raw_string_content); - content.as_str().to_owned() - } - _ => { - panic!("unexpected string literal rule: {:?}", pair.as_str()); - } - } -} - -/// Checks if the text is a valid identifier -pub fn is_identifier(text: &str) -> bool { - match RevsetParser::parse(Rule::identifier, text) { - Ok(mut pairs) => pairs.next().unwrap().as_span().end() == text.len(), - Err(_) => false, - } -} - -/// Parses the text as a revset symbol, rejects empty string. -pub fn parse_symbol(text: &str) -> Result { - let mut pairs = RevsetParser::parse(Rule::symbol_name, text)?; - let first = pairs.next().unwrap(); - let span = first.as_span(); - let name = parse_as_string_literal(first); - if name.is_empty() { - Err(RevsetParseError::expression( - "Expected non-empty string", - span, - )) - } else { - Ok(name) - } -} - -pub type RevsetAliasesMap = AliasesMap; - -#[derive(Clone, Debug, Default)] -pub struct RevsetAliasParser; - -impl AliasDeclarationParser for RevsetAliasParser { - type Error = RevsetParseError; - - fn parse_declaration(&self, source: &str) -> Result { - let mut pairs = RevsetParser::parse(Rule::alias_declaration, source)?; - let first = pairs.next().unwrap(); - match first.as_rule() { - Rule::strict_identifier => Ok(AliasDeclaration::Symbol(first.as_str().to_owned())), - Rule::pattern_alias_declaration => { - let [name_pair, op, param_pair] = first.into_inner().collect_array().unwrap(); - assert_eq!(name_pair.as_rule(), Rule::strict_identifier); - assert_eq!(op.as_rule(), Rule::pattern_kind_op); - assert_eq!(param_pair.as_rule(), Rule::strict_identifier); - let name = name_pair.as_str().to_owned(); - let param = param_pair.as_str().to_owned(); - Ok(AliasDeclaration::Pattern(name, param)) - } - Rule::function_alias_declaration => { - let [name_pair, params_pair] = first.into_inner().collect_array().unwrap(); - assert_eq!(name_pair.as_rule(), Rule::function_name); - assert_eq!(params_pair.as_rule(), Rule::formal_parameters); - let name = name_pair.as_str().to_owned(); - let params_span = params_pair.as_span(); - let params = params_pair - .into_inner() - .map(|pair| match pair.as_rule() { - Rule::strict_identifier => pair.as_str().to_owned(), - r => panic!("unexpected formal parameter rule {r:?}"), - }) - .collect_vec(); - if params.iter().all_unique() { - Ok(AliasDeclaration::Function(name, params)) - } else { - Err(RevsetParseError::with_span( - RevsetParseErrorKind::RedefinedFunctionParameter, - params_span, - )) - } - } - r => panic!("unexpected alias declaration rule {r:?}"), - } - } -} - -impl AliasDefinitionParser for RevsetAliasParser { - type Output<'i> = ExpressionKind<'i>; - type Error = RevsetParseError; - - fn parse_definition<'i>(&self, source: &'i str) -> Result, Self::Error> { - parse_program(source) - } -} - -pub(super) fn expect_string_pattern<'a>( - type_name: &str, - node: &'a ExpressionNode<'_>, -) -> Result<(&'a str, Option<&'a str>), RevsetParseError> { - catch_aliases_no_diagnostics(node, |node| match &node.kind { - ExpressionKind::Identifier(name) => Ok((*name, None)), - ExpressionKind::String(name) => Ok((name, None)), - ExpressionKind::Pattern(pattern) => { - let value = expect_string_literal("string", &pattern.value)?; - Ok((value, Some(pattern.name))) - } - _ => Err(RevsetParseError::expression( - format!("Expected {type_name}"), - node.span, - )), - }) -} - -pub fn expect_literal( - type_name: &str, - node: &ExpressionNode, -) -> Result { - catch_aliases_no_diagnostics(node, |node| { - let value = expect_string_literal(type_name, node)?; - value - .parse() - .map_err(|_| RevsetParseError::expression(format!("Expected {type_name}"), node.span)) - }) -} - -pub(super) fn expect_string_literal<'a>( - type_name: &str, - node: &'a ExpressionNode<'_>, -) -> Result<&'a str, RevsetParseError> { - catch_aliases_no_diagnostics(node, |node| match &node.kind { - ExpressionKind::Identifier(name) => Ok(*name), - ExpressionKind::String(name) => Ok(name), - _ => Err(RevsetParseError::expression( - format!("Expected {type_name}"), - node.span, - )), - }) -} - -/// Applies the given function to the innermost `node` by unwrapping alias -/// expansion nodes. Appends alias expansion stack to error and diagnostics. -pub(super) fn catch_aliases<'a, 'i, T>( - diagnostics: &mut RevsetDiagnostics, - node: &'a ExpressionNode<'i>, - f: impl FnOnce(&mut RevsetDiagnostics, &'a ExpressionNode<'i>) -> Result, -) -> Result { - let (node, stack) = skip_aliases(node); - if stack.is_empty() { - f(diagnostics, node) - } else { - let mut inner_diagnostics = RevsetDiagnostics::new(); - let result = f(&mut inner_diagnostics, node); - diagnostics.extend_with(inner_diagnostics, |diag| attach_aliases_err(diag, &stack)); - result.map_err(|err| attach_aliases_err(err, &stack)) - } -} - -fn catch_aliases_no_diagnostics<'a, 'i, T>( - node: &'a ExpressionNode<'i>, - f: impl FnOnce(&'a ExpressionNode<'i>) -> Result, -) -> Result { - let (node, stack) = skip_aliases(node); - f(node).map_err(|err| attach_aliases_err(err, &stack)) -} - -fn skip_aliases<'a, 'i>( - mut node: &'a ExpressionNode<'i>, -) -> (&'a ExpressionNode<'i>, Vec<(AliasId<'i>, pest::Span<'i>)>) { - let mut stack = Vec::new(); - while let ExpressionKind::AliasExpanded(id, subst) = &node.kind { - stack.push((*id, node.span)); - node = subst; - } - (node, stack) -} - -fn attach_aliases_err( - err: RevsetParseError, - stack: &[(AliasId<'_>, pest::Span<'_>)], -) -> RevsetParseError { - stack - .iter() - .rfold(err, |err, &(id, span)| err.within_alias_expansion(id, span)) -} - -#[cfg(test)] -mod tests { - use std::collections::HashMap; - - use assert_matches::assert_matches; - - use super::*; - use crate::dsl_util::KeywordArgument; - use crate::tests::TestResult; - - #[derive(Debug)] - struct WithRevsetAliasesMap<'i> { - aliases_map: RevsetAliasesMap, - locals: HashMap<&'i str, ExpressionNode<'i>>, - } - - impl<'i> WithRevsetAliasesMap<'i> { - fn set_local(mut self, name: &'i str, value: &'i str) -> Self { - self.locals.insert(name, parse_program(value).unwrap()); - self - } - - fn parse(&'i self, text: &'i str) -> Result, RevsetParseError> { - let node = parse_program(text)?; - dsl_util::expand_aliases_with_locals(node, &self.aliases_map, &self.locals) - } - - fn parse_normalized(&'i self, text: &'i str) -> ExpressionNode<'i> { - normalize_tree(self.parse(text).unwrap()) - } - } - - fn with_aliases<'i>( - aliases: impl IntoIterator, impl Into)>, - ) -> WithRevsetAliasesMap<'i> { - let mut aliases_map = RevsetAliasesMap::new(); - for (decl, defn) in aliases { - aliases_map.insert(decl, defn, None).unwrap(); - } - WithRevsetAliasesMap { - aliases_map, - locals: HashMap::new(), - } - } - - fn parse_into_kind(text: &str) -> Result, RevsetParseErrorKind> { - parse_program(text) - .map(|node| node.kind) - .map_err(|err| *err.kind) - } - - fn parse_normalized(text: &str) -> ExpressionNode<'_> { - normalize_tree(parse_program(text).unwrap()) - } - - /// Drops auxiliary data from parsed tree so it can be compared with other. - fn normalize_tree(node: ExpressionNode) -> ExpressionNode { - fn empty_span() -> pest::Span<'static> { - pest::Span::new("", 0, 0).unwrap() - } - - fn normalize_list(nodes: Vec) -> Vec { - nodes.into_iter().map(normalize_tree).collect() - } - - fn normalize_function_call(function: FunctionCallNode) -> FunctionCallNode { - FunctionCallNode { - name: function.name, - name_span: empty_span(), - args: normalize_list(function.args), - keyword_args: function - .keyword_args - .into_iter() - .map(|arg| KeywordArgument { - name: arg.name, - name_span: empty_span(), - value: normalize_tree(arg.value), - }) - .collect(), - args_span: empty_span(), - } - } - - let normalized_kind = match node.kind { - ExpressionKind::Identifier(_) | ExpressionKind::String(_) => node.kind, - ExpressionKind::Pattern(pattern) => { - let pattern = Box::new(PatternNode { - name: pattern.name, - name_span: empty_span(), - value: normalize_tree(pattern.value), - }); - ExpressionKind::Pattern(pattern) - } - ExpressionKind::RemoteSymbol(_) - | ExpressionKind::AtWorkspace(_) - | ExpressionKind::AtCurrentWorkspace - | ExpressionKind::DagRangeAll - | ExpressionKind::RangeAll => node.kind, - ExpressionKind::Unary(op, arg) => { - let arg = Box::new(normalize_tree(*arg)); - ExpressionKind::Unary(op, arg) - } - ExpressionKind::Binary(op, lhs, rhs) => { - let lhs = Box::new(normalize_tree(*lhs)); - let rhs = Box::new(normalize_tree(*rhs)); - ExpressionKind::Binary(op, lhs, rhs) - } - ExpressionKind::UnionAll(nodes) => { - let nodes = normalize_list(nodes); - ExpressionKind::UnionAll(nodes) - } - ExpressionKind::FunctionCall(function) => { - let function = Box::new(normalize_function_call(*function)); - ExpressionKind::FunctionCall(function) - } - ExpressionKind::AliasExpanded(_, subst) => normalize_tree(*subst).kind, - }; - ExpressionNode { - kind: normalized_kind, - span: empty_span(), - } - } - - #[test] - fn test_parse_tree_eq() { - assert_eq!( - parse_normalized(r#" foo( x ) | ~bar:"baz" "#), - parse_normalized(r#"(foo(x))|(~(bar:"baz"))"#) - ); - assert_ne!(parse_normalized(r#" foo "#), parse_normalized(r#" "foo" "#)); - } - - #[test] - fn test_parse_revset() -> TestResult { - // Parse a quoted symbol - assert_eq!( - parse_into_kind("\"foo\""), - Ok(ExpressionKind::String("foo".to_owned())) - ); - assert_eq!( - parse_into_kind("'foo'"), - Ok(ExpressionKind::String("foo".to_owned())) - ); - // Parse the "parents" operator - assert_matches!( - parse_into_kind("foo-"), - Ok(ExpressionKind::Unary(UnaryOp::Parents, _)) - ); - // Parse the "children" operator - assert_matches!( - parse_into_kind("foo+"), - Ok(ExpressionKind::Unary(UnaryOp::Children, _)) - ); - // Parse the "ancestors" operator - assert_matches!( - parse_into_kind("::foo"), - Ok(ExpressionKind::Unary(UnaryOp::DagRangePre, _)) - ); - // Parse the "descendants" operator - assert_matches!( - parse_into_kind("foo::"), - Ok(ExpressionKind::Unary(UnaryOp::DagRangePost, _)) - ); - // Parse the "dag range" operator - assert_matches!( - parse_into_kind("foo::bar"), - Ok(ExpressionKind::Binary(BinaryOp::DagRange, _, _)) - ); - // Parse the nullary "dag range" operator - assert_matches!(parse_into_kind("::"), Ok(ExpressionKind::DagRangeAll)); - // Parse the "range" prefix operator - assert_matches!( - parse_into_kind("..foo"), - Ok(ExpressionKind::Unary(UnaryOp::RangePre, _)) - ); - assert_matches!( - parse_into_kind("foo.."), - Ok(ExpressionKind::Unary(UnaryOp::RangePost, _)) - ); - assert_matches!( - parse_into_kind("foo..bar"), - Ok(ExpressionKind::Binary(BinaryOp::Range, _, _)) - ); - // Parse the nullary "range" operator - assert_matches!(parse_into_kind(".."), Ok(ExpressionKind::RangeAll)); - // Parse the "negate" operator - assert_matches!( - parse_into_kind("~ foo"), - Ok(ExpressionKind::Unary(UnaryOp::Negate, _)) - ); - assert_eq!( - parse_normalized("~ ~~ foo"), - parse_normalized("~(~(~(foo)))"), - ); - // Parse the "intersection" operator - assert_matches!( - parse_into_kind("foo & bar"), - Ok(ExpressionKind::Binary(BinaryOp::Intersection, _, _)) - ); - // Parse the "union" operator - assert_matches!( - parse_into_kind("foo | bar"), - Ok(ExpressionKind::UnionAll(nodes)) if nodes.len() == 2 - ); - assert_matches!( - parse_into_kind("foo | bar | baz"), - Ok(ExpressionKind::UnionAll(nodes)) if nodes.len() == 3 - ); - // Parse the "difference" operator - assert_matches!( - parse_into_kind("foo ~ bar"), - Ok(ExpressionKind::Binary(BinaryOp::Difference, _, _)) - ); - // Parentheses are allowed before suffix operators - assert_eq!(parse_normalized("(foo)-"), parse_normalized("foo-")); - // Space is allowed around expressions - assert_eq!(parse_normalized(" ::foo "), parse_normalized("::foo")); - assert_eq!(parse_normalized("( ::foo )"), parse_normalized("::foo")); - // Space is not allowed around prefix operators - assert_eq!( - parse_into_kind(" :: foo "), - Err(RevsetParseErrorKind::SyntaxError) - ); - // Incomplete parse - assert_eq!( - parse_into_kind("foo | -"), - Err(RevsetParseErrorKind::SyntaxError) - ); - - // Expression span - assert_eq!(parse_program(" ~ x ")?.span.as_str(), "~ x"); - assert_eq!(parse_program(" x+ ")?.span.as_str(), "x+"); - assert_eq!(parse_program(" x |y ")?.span.as_str(), "x |y"); - assert_eq!(parse_program(" (x) ")?.span.as_str(), "(x)"); - assert_eq!(parse_program("~( x|y) ")?.span.as_str(), "~( x|y)"); - assert_eq!(parse_program(" ( x )- ")?.span.as_str(), "( x )-"); - Ok(()) - } - - #[test] - fn test_parse_whitespace() { - let ascii_whitespaces: String = ('\x00'..='\x7f') - .filter(char::is_ascii_whitespace) - .collect(); - assert_eq!( - parse_normalized(&format!("{ascii_whitespaces}all()")), - parse_normalized("all()"), - ); - } - - #[test] - fn test_parse_identifier() { - // Integer is a symbol - assert_eq!(parse_into_kind("0"), Ok(ExpressionKind::Identifier("0"))); - // Tag/bookmark name separated by / - assert_eq!( - parse_into_kind("foo_bar/baz"), - Ok(ExpressionKind::Identifier("foo_bar/baz")) - ); - // Glob literal with star - assert_eq!( - parse_into_kind("*/foo/**"), - Ok(ExpressionKind::Identifier("*/foo/**")) - ); - - // Internal '.', '-', and '+' are allowed - assert_eq!( - parse_into_kind("foo.bar-v1+7"), - Ok(ExpressionKind::Identifier("foo.bar-v1+7")) - ); - assert_eq!( - parse_normalized("foo.bar-v1+7-"), - parse_normalized("(foo.bar-v1+7)-") - ); - // Multiple '-' are allowed - assert_eq!( - parse_into_kind("foo--bar"), - Ok(ExpressionKind::Identifier("foo--bar")) - ); - assert_eq!( - parse_into_kind("foo----bar"), - Ok(ExpressionKind::Identifier("foo----bar")) - ); - // '.' is not allowed at the beginning or end - assert_eq!( - parse_into_kind(".foo"), - Err(RevsetParseErrorKind::SyntaxError) - ); - assert_eq!( - parse_into_kind("foo."), - Err(RevsetParseErrorKind::SyntaxError) - ); - // Multiple '.' and '+', or together with '-', are not allowed - assert_eq!( - parse_into_kind("foo.+bar"), - Err(RevsetParseErrorKind::SyntaxError) - ); - assert_eq!( - parse_into_kind("foo++bar"), - Err(RevsetParseErrorKind::SyntaxError) - ); - assert_eq!( - parse_into_kind("foo+-bar"), - Err(RevsetParseErrorKind::SyntaxError) - ); - - // Parse a parenthesized symbol - assert_eq!(parse_normalized("(foo)"), parse_normalized("foo")); - - // Non-ASCII tag/bookmark name - assert_eq!( - parse_into_kind("柔術+jj"), - Ok(ExpressionKind::Identifier("柔術+jj")) - ); - } - - #[test] - fn test_parse_string_literal() { - // "\" escapes - assert_eq!( - parse_into_kind(r#" "\t\r\n\"\\\0\e" "#), - Ok(ExpressionKind::String("\t\r\n\"\\\0\u{1b}".to_owned())) - ); - - // Invalid "\" escape - assert_eq!( - parse_into_kind(r#" "\y" "#), - Err(RevsetParseErrorKind::SyntaxError) - ); - - // Single-quoted raw string - assert_eq!( - parse_into_kind(r#" '' "#), - Ok(ExpressionKind::String("".to_owned())) - ); - assert_eq!( - parse_into_kind(r#" 'a\n' "#), - Ok(ExpressionKind::String(r"a\n".to_owned())) - ); - assert_eq!( - parse_into_kind(r#" '\' "#), - Ok(ExpressionKind::String(r"\".to_owned())) - ); - assert_eq!( - parse_into_kind(r#" '"' "#), - Ok(ExpressionKind::String(r#"""#.to_owned())) - ); - - // Hex bytes - assert_eq!( - parse_into_kind(r#""\x61\x65\x69\x6f\x75""#), - Ok(ExpressionKind::String("aeiou".to_owned())) - ); - assert_eq!( - parse_into_kind(r#""\xe0\xe8\xec\xf0\xf9""#), - Ok(ExpressionKind::String("àèìðù".to_owned())) - ); - assert_eq!( - parse_into_kind(r#""\x""#), - Err(RevsetParseErrorKind::SyntaxError) - ); - assert_eq!( - parse_into_kind(r#""\xf""#), - Err(RevsetParseErrorKind::SyntaxError) - ); - assert_eq!( - parse_into_kind(r#""\xgg""#), - Err(RevsetParseErrorKind::SyntaxError) - ); - } - - #[test] - fn test_parse_pattern() -> TestResult { - fn unwrap_pattern(kind: ExpressionKind<'_>) -> (&str, ExpressionKind<'_>) { - match kind { - ExpressionKind::Pattern(pattern) => (pattern.name, pattern.value.kind), - _ => panic!("unexpected expression: {kind:?}"), - } - } - - assert_eq!( - unwrap_pattern(parse_into_kind(r#"substring:"foo""#)?), - ("substring", ExpressionKind::String("foo".to_owned())) - ); - assert_eq!( - unwrap_pattern(parse_into_kind("exact:foo")?), - ("exact", ExpressionKind::Identifier("foo")) - ); - assert_eq!( - parse_into_kind(r#""exact:foo""#), - Ok(ExpressionKind::String("exact:foo".to_owned())) - ); - // Symbol-like value expressions - assert_eq!( - unwrap_pattern(parse_into_kind("x:@")?), - ("x", ExpressionKind::AtCurrentWorkspace) - ); - assert_eq!( - unwrap_pattern(parse_into_kind("x:y@z")?), - ( - "x", - ExpressionKind::RemoteSymbol(RemoteRefSymbolBuf { - name: "y".into(), - remote: "z".into(), - }) - ) - ); - - assert_eq!( - parse_normalized(r#"(exact:"foo" )"#), - parse_normalized(r#"(exact:"foo")"#), - ); - assert_eq!( - unwrap_pattern(parse_into_kind(r#"exact:'\'"#)?), - ("exact", ExpressionKind::String(r"\".to_owned())) - ); - - // Whitespace isn't allowed in between - assert_matches!( - parse_into_kind("exact: foo"), - Err(RevsetParseErrorKind::SyntaxError) - ); - assert_matches!( - parse_into_kind("exact :foo"), - Err(RevsetParseErrorKind::SyntaxError) - ); - // Whitespace is allowed in parenthesized value expression - assert_eq!( - parse_normalized("exact:( 'foo' )"), - parse_normalized("exact:'foo'"), - ); - - // Functions are allowed - assert_eq!(parse_normalized("x:f(y)"), parse_normalized("x:(f(y))")); - // Neighbor postfix operations are also allowed - assert_eq!(parse_normalized("x:@-+"), parse_normalized("x:((@-)+)")); - // Ranges have lower binding strength because we wouldn't want to parse - // x::: as x:(::) - assert_eq!(parse_normalized("x:y::z"), parse_normalized("(x:y)::(z)")); - assert_matches!( - parse_into_kind("x:::"), - Err(RevsetParseErrorKind::SyntaxError) - ); - // Logical operators have lower binding strength - assert_eq!(parse_normalized("x:y&z"), parse_normalized("(x:y)&(z)")); - assert_matches!( - parse_into_kind("x:~y"), // (x:) ~ (y) - Err(RevsetParseErrorKind::NotPostfixOperator { .. }) - ); - - // Pattern prefix is like (type)x cast, so is evaluated from right - assert_eq!(parse_normalized("x:y:z"), parse_normalized("x:(y:z)")); - Ok(()) - } - - #[test] - fn test_parse_symbol_explicitly() { - assert_matches!(parse_symbol("").as_deref(), Err(_)); - // empty string could be a valid ref name, but it would be super - // confusing if identifier was empty. - assert_matches!(parse_symbol("''").as_deref(), Err(_)); - - assert_matches!(parse_symbol("foo.bar").as_deref(), Ok("foo.bar")); - assert_matches!(parse_symbol("foo@bar").as_deref(), Err(_)); - assert_matches!(parse_symbol("foo bar").as_deref(), Err(_)); - - assert_matches!(parse_symbol("'foo bar'").as_deref(), Ok("foo bar")); - assert_matches!(parse_symbol(r#""foo\tbar""#).as_deref(), Ok("foo\tbar")); - - // leading/trailing whitespace is NOT ignored. - assert_matches!(parse_symbol(" foo").as_deref(), Err(_)); - assert_matches!(parse_symbol("foo ").as_deref(), Err(_)); - - // (foo) could be parsed as a symbol "foo", but is rejected because user - // might expect a literal "(foo)". - assert_matches!(parse_symbol("(foo)").as_deref(), Err(_)); - } - - #[test] - fn parse_at_workspace_and_remote_symbol() { - // Parse "@" (the current working copy) - assert_eq!(parse_into_kind("@"), Ok(ExpressionKind::AtCurrentWorkspace)); - assert_eq!( - parse_into_kind("main@"), - Ok(ExpressionKind::AtWorkspace("main".to_owned())) - ); - assert_eq!( - parse_into_kind("main@origin"), - Ok(ExpressionKind::RemoteSymbol(RemoteRefSymbolBuf { - name: "main".into(), - remote: "origin".into() - })) - ); - - // Quoted component in @ expression - assert_eq!( - parse_into_kind(r#""foo bar"@"#), - Ok(ExpressionKind::AtWorkspace("foo bar".to_owned())) - ); - assert_eq!( - parse_into_kind(r#""foo bar"@origin"#), - Ok(ExpressionKind::RemoteSymbol(RemoteRefSymbolBuf { - name: "foo bar".into(), - remote: "origin".into() - })) - ); - assert_eq!( - parse_into_kind(r#"main@"foo bar""#), - Ok(ExpressionKind::RemoteSymbol(RemoteRefSymbolBuf { - name: "main".into(), - remote: "foo bar".into() - })) - ); - assert_eq!( - parse_into_kind(r#"'foo bar'@'bar baz'"#), - Ok(ExpressionKind::RemoteSymbol(RemoteRefSymbolBuf { - name: "foo bar".into(), - remote: "bar baz".into() - })) - ); - - // Quoted "@" is not interpreted as a working copy or remote symbol - assert_eq!( - parse_into_kind(r#""@""#), - Ok(ExpressionKind::String("@".to_owned())) - ); - assert_eq!( - parse_into_kind(r#""main@""#), - Ok(ExpressionKind::String("main@".to_owned())) - ); - assert_eq!( - parse_into_kind(r#""main@origin""#), - Ok(ExpressionKind::String("main@origin".to_owned())) - ); - - // Non-ASCII name - assert_eq!( - parse_into_kind("柔術@"), - Ok(ExpressionKind::AtWorkspace("柔術".to_owned())) - ); - assert_eq!( - parse_into_kind("柔@術"), - Ok(ExpressionKind::RemoteSymbol(RemoteRefSymbolBuf { - name: "柔".into(), - remote: "術".into() - })) - ); - } - - #[test] - fn test_parse_function_call() -> TestResult { - fn unwrap_function_call(node: ExpressionNode<'_>) -> Box> { - match node.kind { - ExpressionKind::FunctionCall(function) => function, - _ => panic!("unexpected expression: {node:?}"), - } - } - - // Space is allowed around infix operators and function arguments - assert_eq!( - parse_normalized( - " description( arg1 ) ~ file( arg1 , arg2 ) ~ visible_heads( ) ", - ), - parse_normalized("(description(arg1) ~ file(arg1, arg2)) ~ visible_heads()"), - ); - // Space is allowed around keyword arguments - assert_eq!( - parse_normalized("remote_bookmarks( remote = foo )"), - parse_normalized("remote_bookmarks(remote=foo)"), - ); - - // Trailing comma isn't allowed for empty argument - assert!(parse_into_kind("bookmarks(,)").is_err()); - // Trailing comma is allowed for the last argument - assert_eq!( - parse_normalized("bookmarks(a,)"), - parse_normalized("bookmarks(a)") - ); - assert_eq!( - parse_normalized("bookmarks(a , )"), - parse_normalized("bookmarks(a)") - ); - assert!(parse_into_kind("bookmarks(,a)").is_err()); - assert!(parse_into_kind("bookmarks(a,,)").is_err()); - assert!(parse_into_kind("bookmarks(a , , )").is_err()); - assert_eq!( - parse_normalized("file(a,b,)"), - parse_normalized("file(a, b)") - ); - assert!(parse_into_kind("file(a,,b)").is_err()); - assert_eq!( - parse_normalized("remote_bookmarks(a,remote=b , )"), - parse_normalized("remote_bookmarks(a, remote=b)"), - ); - assert!(parse_into_kind("remote_bookmarks(a,,remote=b)").is_err()); - - // Expression span - let function = unwrap_function_call(parse_program("foo( a, (b) , ~(c), d = (e) )")?); - assert_eq!(function.name_span.as_str(), "foo"); - assert_eq!(function.args_span.as_str(), "a, (b) , ~(c), d = (e)"); - assert_eq!(function.args[0].span.as_str(), "a"); - assert_eq!(function.args[1].span.as_str(), "(b)"); - assert_eq!(function.args[2].span.as_str(), "~(c)"); - assert_eq!(function.keyword_args[0].name_span.as_str(), "d"); - assert_eq!(function.keyword_args[0].value.span.as_str(), "(e)"); - Ok(()) - } - - #[test] - fn test_parse_revset_alias_symbol_decl() { - let mut aliases_map = RevsetAliasesMap::new(); - // Working copy or remote symbol cannot be used as an alias name. - assert!(aliases_map.insert("@", "none()", None).is_err()); - assert!(aliases_map.insert("a@", "none()", None).is_err()); - assert!(aliases_map.insert("a@b", "none()", None).is_err()); - // Non-ASCII character isn't allowed in alias symbol. This rule can be - // relaxed if needed. - assert!(aliases_map.insert("柔術", "none()", None).is_err()); - } - - #[test] - fn test_parse_revset_alias_pattern_decl() -> TestResult { - let mut aliases_map = RevsetAliasesMap::new(); - assert!(aliases_map.insert("foo:", "none()", None).is_err()); - assert_eq!(aliases_map.pattern_names().count(), 0); - - aliases_map.insert("bar:baz", "'bar pattern'", None)?; - assert_eq!(aliases_map.pattern_names().count(), 1); - let (id, param, defn, _doc) = aliases_map.get_pattern("bar").unwrap(); - assert_eq!(id, AliasId::Pattern("bar", "baz")); - assert_eq!(param, "baz"); - assert_eq!(defn, "'bar pattern'"); - - // Non-ASCII character isn't allowed. This rule can be relaxed if - // needed. - assert!(aliases_map.insert("柔術:x", "none()", None).is_err()); - assert!(aliases_map.insert("x:柔術", "none()", None).is_err()); - Ok(()) - } - - #[test] - fn test_parse_revset_alias_func_decl() -> TestResult { - let mut aliases_map = RevsetAliasesMap::new(); - assert!( - aliases_map - .insert("5func()", r#""is function 0""#, None) - .is_err() - ); - aliases_map.insert("func()", r#""is function 0""#, None)?; - aliases_map.insert("func(a, b)", r#""is function 2""#, None)?; - aliases_map.insert("func(a)", r#""is function a""#, None)?; - aliases_map.insert("func(b)", r#""is function b""#, None)?; - - let (id, params, defn, _doc) = aliases_map.get_function("func", 0).unwrap(); - assert_eq!(id, AliasId::Function("func", &[])); - assert!(params.is_empty()); - assert_eq!(defn, r#""is function 0""#); - - let (id, params, defn, _doc) = aliases_map.get_function("func", 1).unwrap(); - assert_eq!(id, AliasId::Function("func", &["b".to_owned()])); - assert_eq!(params, ["b"]); - assert_eq!(defn, r#""is function b""#); - - let (id, params, defn, _doc) = aliases_map.get_function("func", 2).unwrap(); - assert_eq!( - id, - AliasId::Function("func", &["a".to_owned(), "b".to_owned()]) - ); - assert_eq!(params, ["a", "b"]); - assert_eq!(defn, r#""is function 2""#); - - assert!(aliases_map.get_function("func", 3).is_none()); - Ok(()) - } - - #[test] - fn test_parse_revset_alias_formal_parameter() { - let mut aliases_map = RevsetAliasesMap::new(); - // Working copy or remote symbol cannot be used as an parameter name. - assert!(aliases_map.insert("f(@)", "none()", None).is_err()); - assert!(aliases_map.insert("f(a@)", "none()", None).is_err()); - assert!(aliases_map.insert("f(a@b)", "none()", None).is_err()); - // Trailing comma isn't allowed for empty parameter - assert!(aliases_map.insert("f(,)", "none()", None).is_err()); - // Trailing comma is allowed for the last parameter - assert!(aliases_map.insert("g(a,)", "none()", None).is_ok()); - assert!(aliases_map.insert("h(a , )", "none()", None).is_ok()); - assert!(aliases_map.insert("i(,a)", "none()", None).is_err()); - assert!(aliases_map.insert("j(a,,)", "none()", None).is_err()); - assert!(aliases_map.insert("k(a , , )", "none()", None).is_err()); - assert!(aliases_map.insert("l(a,b,)", "none()", None).is_ok()); - assert!(aliases_map.insert("m(a,,b)", "none()", None).is_err()); - } - - #[test] - fn test_parse_revset_compat_operator() { - assert_eq!( - parse_into_kind(":foo"), - Err(RevsetParseErrorKind::NotPrefixOperator { - op: ":".to_owned(), - similar_op: "::".to_owned(), - description: "ancestors".to_owned(), - }) - ); - assert_eq!( - parse_into_kind("foo^"), - Err(RevsetParseErrorKind::NotPostfixOperator { - op: "^".to_owned(), - similar_op: "-".to_owned(), - description: "parents".to_owned(), - }) - ); - assert_eq!( - parse_into_kind("foo + bar"), - Err(RevsetParseErrorKind::NotInfixOperator { - op: "+".to_owned(), - similar_op: "|".to_owned(), - description: "union".to_owned(), - }) - ); - assert_eq!( - parse_into_kind("foo - bar"), - Err(RevsetParseErrorKind::NotInfixOperator { - op: "-".to_owned(), - similar_op: "~".to_owned(), - description: "difference".to_owned(), - }) - ); - } - - #[test] - fn test_parse_revset_operator_combinations() { - // Parse repeated "parents" operator - assert_eq!(parse_normalized("foo---"), parse_normalized("((foo-)-)-")); - // Parse repeated "children" operator - assert_eq!(parse_normalized("foo+++"), parse_normalized("((foo+)+)+")); - // Set operator associativity/precedence - assert_eq!(parse_normalized("~x|y"), parse_normalized("(~x)|y")); - assert_eq!(parse_normalized("x&~y"), parse_normalized("x&(~y)")); - assert_eq!(parse_normalized("x~~y"), parse_normalized("x~(~y)")); - assert_eq!(parse_normalized("x~~~y"), parse_normalized("x~(~(~y))")); - assert_eq!(parse_normalized("~x::y"), parse_normalized("~(x::y)")); - assert_eq!(parse_normalized("x|y|z"), parse_normalized("(x|y)|z")); - assert_eq!(parse_normalized("x&y|z"), parse_normalized("(x&y)|z")); - assert_eq!(parse_normalized("x|y&z"), parse_normalized("x|(y&z)")); - assert_eq!(parse_normalized("x|y~z"), parse_normalized("x|(y~z)")); - assert_eq!(parse_normalized("::&.."), parse_normalized("(::)&(..)")); - // Parse repeated "ancestors"/"descendants"/"dag range"/"range" operators - assert_eq!( - parse_into_kind("::foo::"), - Err(RevsetParseErrorKind::SyntaxError) - ); - assert_eq!( - parse_into_kind(":::foo"), - Err(RevsetParseErrorKind::SyntaxError) - ); - assert_eq!( - parse_into_kind("::::foo"), - Err(RevsetParseErrorKind::SyntaxError) - ); - assert_eq!( - parse_into_kind("foo:::"), - Err(RevsetParseErrorKind::SyntaxError) - ); - assert_eq!( - parse_into_kind("foo::::"), - Err(RevsetParseErrorKind::SyntaxError) - ); - assert_eq!( - parse_into_kind("foo:::bar"), - Err(RevsetParseErrorKind::SyntaxError) - ); - assert_eq!( - parse_into_kind("foo::::bar"), - Err(RevsetParseErrorKind::SyntaxError) - ); - assert_eq!( - parse_into_kind("::foo::bar"), - Err(RevsetParseErrorKind::SyntaxError) - ); - assert_eq!( - parse_into_kind("foo::bar::"), - Err(RevsetParseErrorKind::SyntaxError) - ); - assert_eq!( - parse_into_kind("::::"), - Err(RevsetParseErrorKind::SyntaxError) - ); - assert_eq!( - parse_into_kind("....foo"), - Err(RevsetParseErrorKind::SyntaxError) - ); - assert_eq!( - parse_into_kind("foo...."), - Err(RevsetParseErrorKind::SyntaxError) - ); - assert_eq!( - parse_into_kind("foo.....bar"), - Err(RevsetParseErrorKind::SyntaxError) - ); - assert_eq!( - parse_into_kind("..foo..bar"), - Err(RevsetParseErrorKind::SyntaxError) - ); - assert_eq!( - parse_into_kind("foo..bar.."), - Err(RevsetParseErrorKind::SyntaxError) - ); - assert_eq!( - parse_into_kind("...."), - Err(RevsetParseErrorKind::SyntaxError) - ); - assert_eq!( - parse_into_kind("::.."), - Err(RevsetParseErrorKind::SyntaxError) - ); - // Parse combinations of "parents"/"children" operators and the range operators. - // The former bind more strongly. - assert_eq!(parse_normalized("foo-+"), parse_normalized("(foo-)+")); - assert_eq!(parse_normalized("foo-::"), parse_normalized("(foo-)::")); - assert_eq!(parse_normalized("::foo+"), parse_normalized("::(foo+)")); - assert_eq!( - parse_into_kind("::-"), - Err(RevsetParseErrorKind::SyntaxError) - ); - assert_eq!( - parse_into_kind("..+"), - Err(RevsetParseErrorKind::SyntaxError) - ); - } - - #[test] - fn test_parse_revset_function() { - assert_matches!( - parse_into_kind("parents(foo)"), - Ok(ExpressionKind::FunctionCall(_)) - ); - assert_eq!( - parse_normalized("parents((foo))"), - parse_normalized("parents(foo)"), - ); - assert_eq!( - parse_into_kind("parents(foo"), - Err(RevsetParseErrorKind::SyntaxError) - ); - } - - #[test] - fn test_expand_symbol_alias() { - assert_eq!( - with_aliases([("AB", "a&b")]).parse_normalized("AB|c"), - parse_normalized("(a&b)|c") - ); - assert_eq!( - with_aliases([("AB", "a|b")]).parse_normalized("AB::heads(AB)"), - parse_normalized("(a|b)::heads(a|b)") - ); - - // Not string substitution 'a&b|c', but tree substitution. - assert_eq!( - with_aliases([("BC", "b|c")]).parse_normalized("a&BC"), - parse_normalized("a&(b|c)") - ); - - // String literal should not be substituted with alias. - assert_eq!( - with_aliases([("A", "a")]).parse_normalized(r#"A|"A"|'A'"#), - parse_normalized("a|'A'|'A'") - ); - - // Kind of string pattern should not be substituted, which is similar to - // function name. - assert_eq!( - with_aliases([("A", "a")]).parse_normalized("author(A:b)"), - parse_normalized("author(A:b)") - ); - - // Value of string pattern can be substituted if it's an identifier. - assert_eq!( - with_aliases([("A", "a")]).parse_normalized("author(exact:A)"), - parse_normalized("author(exact:a)") - ); - assert_eq!( - with_aliases([("A", "a")]).parse_normalized("author(exact:'A')"), - parse_normalized("author(exact:'A')") - ); - - // Part of @ symbol cannot be substituted. - assert_eq!( - with_aliases([("A", "a")]).parse_normalized("A@"), - parse_normalized("A@") - ); - assert_eq!( - with_aliases([("A", "a")]).parse_normalized("A@b"), - parse_normalized("A@b") - ); - assert_eq!( - with_aliases([("B", "b")]).parse_normalized("a@B"), - parse_normalized("a@B") - ); - - // Multi-level substitution. - assert_eq!( - with_aliases([("A", "BC"), ("BC", "b|C"), ("C", "c")]).parse_normalized("A"), - parse_normalized("b|c") - ); - - // Infinite recursion, where the top-level error isn't of RecursiveAlias kind. - assert_eq!( - *with_aliases([("A", "A")]).parse("A").unwrap_err().kind, - RevsetParseErrorKind::InAliasExpansion("A".to_owned()) - ); - assert_eq!( - *with_aliases([("A", "B"), ("B", "b|C"), ("C", "c|B")]) - .parse("A") - .unwrap_err() - .kind, - RevsetParseErrorKind::InAliasExpansion("A".to_owned()) - ); - - // Error in alias definition. - assert_eq!( - *with_aliases([("A", "a(")]).parse("A").unwrap_err().kind, - RevsetParseErrorKind::InAliasExpansion("A".to_owned()) - ); - } - - #[test] - fn test_expand_pattern_alias() { - assert_eq!( - with_aliases([("P:x", "x")]).parse_normalized("P:a"), - parse_normalized("a") - ); - - // Argument should be resolved in the current scope. - assert_eq!( - with_aliases([("P:x", "x|a")]).parse_normalized("P:x"), - parse_normalized("x|a") - ); - // P:a -> (Q:a)&y -> (x|a)&y - assert_eq!( - with_aliases([("P:x", "(Q:x)&y"), ("Q:y", "x|y")]).parse_normalized("P:a"), - parse_normalized("(x|a)&y") - ); - - // Pattern parameter should precede the symbol alias. - assert_eq!( - with_aliases([("P:X", "X"), ("X", "x")]).parse_normalized("(P:a)|X"), - parse_normalized("a|x") - ); - - // Pattern parameter shouldn't be expanded in symbol alias. - assert_eq!( - with_aliases([("P:x", "x|A"), ("A", "x")]).parse_normalized("P:a"), - parse_normalized("a|x") - ); - - // String literal should not be substituted with pattern parameter. - assert_eq!( - with_aliases([("P:x", "x|'x'")]).parse_normalized("P:a"), - parse_normalized("a|'x'") - ); - - // Pattern and symbol aliases reside in separate namespaces. - assert_eq!( - with_aliases([("A:x", "A"), ("A", "a")]).parse_normalized("A:x"), - parse_normalized("a") - ); - - // Infinite recursion, where the top-level error isn't of RecursiveAlias kind. - assert_eq!( - *with_aliases([("P:x", "Q:x"), ("Q:x", "R:x"), ("R:x", "P:x")]) - .parse("P:a") - .unwrap_err() - .kind, - RevsetParseErrorKind::InAliasExpansion("P:x".to_owned()) - ); - } - - #[test] - fn test_expand_function_alias() { - assert_eq!( - with_aliases([("F( )", "a")]).parse_normalized("F()"), - parse_normalized("a") - ); - assert_eq!( - with_aliases([("F( x )", "x")]).parse_normalized("F(a)"), - parse_normalized("a") - ); - assert_eq!( - with_aliases([("F( x, y )", "x|y")]).parse_normalized("F(a, b)"), - parse_normalized("a|b") - ); - - // Not recursion because functions are overloaded by arity. - assert_eq!( - with_aliases([("F(x)", "F(x,b)"), ("F(x,y)", "x|y")]).parse_normalized("F(a)"), - parse_normalized("a|b") - ); - - // Arguments should be resolved in the current scope. - assert_eq!( - with_aliases([("F(x,y)", "x|y")]).parse_normalized("F(a::y,b::x)"), - parse_normalized("(a::y)|(b::x)") - ); - // F(a) -> G(a)&y -> (x|a)&y - assert_eq!( - with_aliases([("F(x)", "G(x)&y"), ("G(y)", "x|y")]).parse_normalized("F(a)"), - parse_normalized("(x|a)&y") - ); - // F(G(a)) -> F(x|a) -> G(x|a)&y -> (x|(x|a))&y - assert_eq!( - with_aliases([("F(x)", "G(x)&y"), ("G(y)", "x|y")]).parse_normalized("F(G(a))"), - parse_normalized("(x|(x|a))&y") - ); - - // Function parameter should precede the symbol alias. - assert_eq!( - with_aliases([("F(X)", "X"), ("X", "x")]).parse_normalized("F(a)|X"), - parse_normalized("a|x") - ); - - // Function parameter shouldn't be expanded in symbol alias. - assert_eq!( - with_aliases([("F(x)", "x|A"), ("A", "x")]).parse_normalized("F(a)"), - parse_normalized("a|x") - ); - - // String literal should not be substituted with function parameter. - assert_eq!( - with_aliases([("F(x)", r#"x|"x""#)]).parse_normalized("F(a)"), - parse_normalized("a|'x'") - ); - - // Function and symbol aliases reside in separate namespaces. - assert_eq!( - with_aliases([("A()", "A"), ("A", "a")]).parse_normalized("A()"), - parse_normalized("a") - ); - - // Invalid number of arguments. - assert_eq!( - *with_aliases([("F()", "x")]).parse("F(a)").unwrap_err().kind, - RevsetParseErrorKind::InvalidFunctionArguments { - name: "F".to_owned(), - message: "Expected 0 arguments".to_owned() - } - ); - assert_eq!( - *with_aliases([("F(x)", "x")]).parse("F()").unwrap_err().kind, - RevsetParseErrorKind::InvalidFunctionArguments { - name: "F".to_owned(), - message: "Expected 1 arguments".to_owned() - } - ); - assert_eq!( - *with_aliases([("F(x,y)", "x|y")]) - .parse("F(a,b,c)") - .unwrap_err() - .kind, - RevsetParseErrorKind::InvalidFunctionArguments { - name: "F".to_owned(), - message: "Expected 2 arguments".to_owned() - } - ); - assert_eq!( - *with_aliases([("F(x)", "x"), ("F(x,y)", "x|y")]) - .parse("F()") - .unwrap_err() - .kind, - RevsetParseErrorKind::InvalidFunctionArguments { - name: "F".to_owned(), - message: "Expected 1 to 2 arguments".to_owned() - } - ); - assert_eq!( - *with_aliases([("F()", "x"), ("F(x,y)", "x|y")]) - .parse("F(a)") - .unwrap_err() - .kind, - RevsetParseErrorKind::InvalidFunctionArguments { - name: "F".to_owned(), - message: "Expected 0, 2 arguments".to_owned() - } - ); - - // Keyword argument isn't supported for now. - assert_eq!( - *with_aliases([("F(x)", "x")]) - .parse("F(x=y)") - .unwrap_err() - .kind, - RevsetParseErrorKind::InvalidFunctionArguments { - name: "F".to_owned(), - message: "Unexpected keyword arguments".to_owned() - } - ); - - // Infinite recursion, where the top-level error isn't of RecursiveAlias kind. - assert_eq!( - *with_aliases([("F(x)", "G(x)"), ("G(x)", "H(x)"), ("H(x)", "F(x)")]) - .parse("F(a)") - .unwrap_err() - .kind, - RevsetParseErrorKind::InAliasExpansion("F(x)".to_owned()) - ); - assert_eq!( - *with_aliases([("F(x)", "F(x,b)"), ("F(x,y)", "F(x|y)")]) - .parse("F(a)") - .unwrap_err() - .kind, - RevsetParseErrorKind::InAliasExpansion("F(x)".to_owned()) - ); - } - - #[test] - fn test_expand_with_locals() { - // Local variable should precede the symbol alias. - assert_eq!( - with_aliases([("A", "symbol")]) - .set_local("A", "local") - .parse_normalized("A"), - parse_normalized("local") - ); - - // Local variable shouldn't be expanded within aliases. - assert_eq!( - with_aliases([("B", "A"), ("F(x)", "x&A")]) - .set_local("A", "a") - .parse_normalized("A|B|F(A)"), - parse_normalized("a|A|(a&A)") - ); - } -} +// This is needed so we export the same interface as usual without people +// noticing that we moved everything to the jj-core crate. +#![expect(unused_imports)] + +pub use jj_core::revset_parser::BinaryOp; +pub use jj_core::revset_parser::ExpressionKind; +pub use jj_core::revset_parser::ExpressionNode; +pub use jj_core::revset_parser::FunctionCallNode; +pub use jj_core::revset_parser::PatternNode; +pub use jj_core::revset_parser::RevsetAliasParser; +pub use jj_core::revset_parser::RevsetAliasesMap; +pub use jj_core::revset_parser::RevsetDiagnostics; +pub use jj_core::revset_parser::RevsetParseError; +pub use jj_core::revset_parser::RevsetParseErrorKind; +pub use jj_core::revset_parser::Rule; +pub use jj_core::revset_parser::UnaryOp; +pub use jj_core::revset_parser::catch_aliases; +pub use jj_core::revset_parser::expect_literal; +pub use jj_core::revset_parser::expect_string_literal; +pub use jj_core::revset_parser::expect_string_pattern; +pub use jj_core::revset_parser::is_identifier; +pub use jj_core::revset_parser::parse_program; +pub use jj_core::revset_parser::parse_symbol; diff --git a/lib/src/signing.rs b/lib/src/signing.rs index 54a9a29045f..f2d403af4fd 100644 --- a/lib/src/signing.rs +++ b/lib/src/signing.rs @@ -15,11 +15,12 @@ //! Generic APIs to work with cryptographic signatures created and verified by //! various backends. -use std::fmt::Debug; -use std::fmt::Display; -use std::sync::Mutex; - -use clru::CLruCache; +pub use jj_core::signing::SigStatus; +pub use jj_core::signing::SignError; +pub use jj_core::signing::SignResult; +use jj_core::signing::Signer as CoreSigner; +pub use jj_core::signing::SigningBackend; +pub use jj_core::signing::Verification; use thiserror::Error; use crate::backend::CommitId; @@ -28,108 +29,9 @@ use crate::gpg_signing::GpgBackend; use crate::gpg_signing::GpgsmBackend; use crate::settings::UserSettings; use crate::ssh_signing::SshBackend; -use crate::store::COMMIT_CACHE_CAPACITY; #[cfg(feature = "testing")] use crate::test_signing_backend::TestSigningBackend; -/// A status of the signature, part of the [Verification] type. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum SigStatus { - /// Valid signature that matches the data. - Good, - /// Valid signature that could not be verified (e.g. due to an unknown key). - Unknown, - /// Valid signature that does not match the signed data. - Bad, -} - -impl Display for SigStatus { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let s = match self { - Self::Good => "good", - Self::Unknown => "unknown", - Self::Bad => "bad", - }; - write!(f, "{s}") - } -} - -/// The result of a signature verification. -/// Key and display are optional additional info that backends can or can not -/// provide to add additional information for the templater to potentially show. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct Verification { - /// The status of the signature. - pub status: SigStatus, - /// The key id representation, if available. For GPG, this will be the key - /// fingerprint. - pub key: Option, - /// A display string, if available. For GPG, this will be formatted primary - /// user ID. - pub display: Option, -} - -impl Verification { - /// A shortcut to create an `Unknown` verification with no additional - /// metadata. - pub fn unknown() -> Self { - Self { - status: SigStatus::Unknown, - key: None, - display: None, - } - } - - /// Create a new verification - pub fn new(status: SigStatus, key: Option, display: Option) -> Self { - Self { - status, - key, - display, - } - } -} - -/// The backend for signing and verifying cryptographic signatures. -/// -/// This allows using different signers, such as GPG or SSH, or different -/// versions of them. -pub trait SigningBackend: Debug + Send + Sync { - /// Name of the backend, used in the config and for display. - fn name(&self) -> &str; - - /// Check if the signature can be read and verified by this backend. - /// - /// Should check the signature format, usually just looks at the prefix. - fn can_read(&self, signature: &[u8]) -> bool; - - /// Create a signature for arbitrary data. - /// - /// The `key` parameter is what `jj sign` receives as key argument, or what - /// is configured in the `signing.key` config. - fn sign(&self, data: &[u8], key: Option<&str>) -> SignResult>; - - /// Verify a signature. Should be reflexive with `sign`: - /// ```rust,ignore - /// verify(data, sign(data)?)?.status == SigStatus::Good - /// ``` - fn verify(&self, data: &[u8], signature: &[u8]) -> SignResult; -} - -/// An error type for the signing/verifying operations -#[derive(Debug, Error)] -pub enum SignError { - /// The verification failed because the signature *format* was invalid. - #[error("Invalid signature")] - InvalidSignatureFormat, - /// A generic error from the backend impl. - #[error("Signing error")] - Backend(#[source] Box), -} - -/// A result type for the signing/verifying operations -pub type SignResult = Result; - /// An error type for the signing backend initialization. #[derive(Debug, Error)] pub enum SignInitError { @@ -164,14 +66,8 @@ pub enum SignBehavior { /// Wraps low-level signing backends and adds caching, similar to `Store`. #[derive(Debug)] pub struct Signer { - /// The backend that is used for signing commits. - /// Optional because signing might not be configured. - main_backend: Option>, - /// All known backends without the main one - used for verification. - /// Main backend is also used for verification, but it's not in this list - /// for ownership reasons. - backends: Vec>, - cache: Mutex>, + /// The CoreSigner contains all fields. + inner: CoreSigner, } impl Signer { @@ -206,25 +102,19 @@ impl Signer { main_backend: Option>, other_backends: Vec>, ) -> Self { - Self { - main_backend, - backends: other_backends, - cache: Mutex::new(CLruCache::new(COMMIT_CACHE_CAPACITY)), - } + let inner = CoreSigner::new(main_backend, other_backends); + Self { inner } } /// Checks if the signer can sign, i.e. if a main backend is configured. pub fn can_sign(&self) -> bool { - self.main_backend.is_some() + self.inner.can_sign() } /// This is just a pass-through to the main backend that unconditionally /// creates a signature. pub fn sign(&self, data: &[u8], key: Option<&str>) -> SignResult> { - self.main_backend - .as_ref() - .expect("tried to sign without checking can_sign first") - .sign(data, key) + self.inner.sign(data, key) } /// Looks for backend that can verify the signature and returns the result @@ -235,46 +125,6 @@ impl Signer { data: &[u8], signature: &[u8], ) -> SignResult { - let cached = self.cache.lock().unwrap().get(commit_id).cloned(); - if let Some(check) = cached { - return Ok(check); - } - - let verification = self - .main_backend - .iter() - .chain(self.backends.iter()) - .filter(|b| b.can_read(signature)) - // skip unknown and invalid sigs to allow other backends that can read to try - // for example, we might have gpg and sq, both of which could read a PGP signature - .find_map(|backend| match backend.verify(data, signature) { - Ok(check) if check.status == SigStatus::Unknown => None, - Err(SignError::InvalidSignatureFormat) => None, - e => Some(e), - }) - .transpose()?; - - if let Some(verification) = verification { - // a key might get imported before next call?. - // realistically this is unlikely, but technically - // it's correct to not cache unknowns here - if verification.status != SigStatus::Unknown { - self.cache - .lock() - .unwrap() - .put(commit_id.clone(), verification.clone()); - } - Ok(verification) - } else { - // now here it's correct to cache unknowns, as we don't - // have a backend that knows how to handle this signature - // - // not sure about how much of an optimization this is - self.cache - .lock() - .unwrap() - .put(commit_id.clone(), Verification::unknown()); - Ok(Verification::unknown()) - } + self.inner.verify(commit_id, data, signature) } } diff --git a/lib/src/workspace_store.rs b/lib/src/workspace_store.rs index 32e6072b4be..2adf9a29392 100644 --- a/lib/src/workspace_store.rs +++ b/lib/src/workspace_store.rs @@ -20,6 +20,8 @@ use std::io::Write as _; use std::path::Path; use std::path::PathBuf; +pub use jj_core::workspace_store::WorkspaceStore; +pub use jj_core::workspace_store::WorkspaceStoreError; use jj_lib::file_util::BadPathEncoding; use jj_lib::file_util::IoResultExt as _; use jj_lib::file_util::PathError; @@ -36,39 +38,6 @@ use prost::Message as _; use tempfile::NamedTempFile; use thiserror::Error; -/// Errors that can occur when interacting with a workspace store. -#[derive(Error, Debug)] -pub enum WorkspaceStoreError { - /// An unspecified error occurred. - #[error(transparent)] - Other(#[from] Box), -} - -/// A storage backend for workspace metadata. -pub trait WorkspaceStore: Send + Sync + Debug { - /// Returns the name of this workspace store implementation. - fn name(&self) -> &str; - - /// Adds a workspace with the given name and path to the store. - fn add(&self, workspace_name: &WorkspaceName, path: &Path) -> Result<(), WorkspaceStoreError>; - - /// Forgets the workspaces with the given names. - fn forget(&self, workspace_names: &[&WorkspaceName]) -> Result<(), WorkspaceStoreError>; - - /// Renames a workspace from `old_name` to `new_name`. - fn rename( - &self, - old_name: &WorkspaceName, - new_name: &WorkspaceName, - ) -> Result<(), WorkspaceStoreError>; - - /// Gets the path of the workspace with the given name, if it exists. - fn get_workspace_path( - &self, - workspace_name: &WorkspaceName, - ) -> Result, WorkspaceStoreError>; -} - /// Errors specific to the `SimpleWorkspaceStore` implementation. #[derive(Error, Debug)] pub enum SimpleWorkspaceStoreError {