├── src ├── rawvalue.rs ├── tests.rs ├── tests │ ├── version.rs │ ├── property.rs │ ├── linereader.rs │ ├── properties.rs │ └── ecparser.rs ├── version.rs ├── fallback.rs ├── properties │ └── iter.rs ├── traits.rs ├── glob.rs ├── error.rs ├── property │ └── language_tag.rs ├── section.rs ├── lib.rs ├── linereader.rs ├── file.rs ├── parser.rs ├── property.rs ├── string.rs └── properties.rs ├── .clippy.toml ├── .gitignore ├── .rustfmt.toml ├── .gitmodules ├── glob ├── src │ ├── parser.rs │ ├── parser │ │ ├── numrange.rs │ │ ├── charclass.rs │ │ ├── main.rs │ │ └── alt.rs │ ├── flatset.rs │ ├── matcher.rs │ ├── lib.rs │ ├── stack.rs │ ├── tests.rs │ └── splitter.rs ├── Cargo.toml └── README.md ├── .editorconfig ├── tools ├── Cargo.toml └── src │ └── bin │ └── ec4rs-parse.rs ├── .github └── workflows │ ├── check-ctest.yml │ ├── check-glob.yml │ └── check-lib.yml ├── Cargo.toml ├── DCO.txt ├── CHANGELOG.md ├── rustdoc.md ├── README.md └── LICENSE.txt /src/rawvalue.rs: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /.clippy.toml: -------------------------------------------------------------------------------- 1 | msrv = "1.56.0" 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock 3 | Testing 4 | -------------------------------------------------------------------------------- /.rustfmt.toml: -------------------------------------------------------------------------------- 1 | max_width = 100 2 | use_field_init_shorthand = true 3 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "tests"] 2 | path = tests 3 | url = https://github.com/editorconfig/editorconfig-core-test.git 4 | -------------------------------------------------------------------------------- /src/tests.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "ec4rs_glob")] 2 | mod ecparser; 3 | mod linereader; 4 | mod properties; 5 | mod property; 6 | mod version; 7 | -------------------------------------------------------------------------------- /src/tests/version.rs: -------------------------------------------------------------------------------- 1 | #[test] 2 | fn string_matches_ints() { 3 | use crate::version::*; 4 | assert_eq!(STRING, format!("{}.{}.{}", MAJOR, MINOR, PATCH)); 5 | } 6 | -------------------------------------------------------------------------------- /glob/src/parser.rs: -------------------------------------------------------------------------------- 1 | mod alt; 2 | mod charclass; 3 | mod main; 4 | mod numrange; 5 | 6 | pub use main::parse; 7 | 8 | type Chars<'a> = std::iter::Peekable>; 9 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | insert_final_newline = true 5 | trim_trailing_whitespace = true 6 | 7 | [*.md] 8 | max_line_length = 78 9 | 10 | [*.rs] 11 | indent_style = space 12 | indent_size = 4 13 | max_line_length = 100 14 | 15 | [*.yml] 16 | indent_style = space 17 | indent_size = 2 18 | -------------------------------------------------------------------------------- /tools/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ec4rs_tools" 3 | version = "1.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | ec4rs = { path = "..", features = ["track-source"] } 10 | semver = "1.0" 11 | clap = { version = "3.1", features = ["derive"] } 12 | 13 | [[bin]] 14 | name = "ec4rs-parse" 15 | -------------------------------------------------------------------------------- /glob/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ec4rs_glob" 3 | description = "Permissive glob engine used by ec4rs" 4 | license = "Apache-2.0" 5 | homepage = "https://github.com/TheDaemoness/ec4rs/tree/main/glob" 6 | repository = "https://github.com/TheDaemoness/ec4rs" 7 | readme = "README.md" 8 | 9 | authors = ["TheDaemoness"] 10 | include = ["/src", "/README.md"] 11 | rust-version = "1.56" # 2021 edition 12 | version = "0.1.0" 13 | edition = "2021" 14 | -------------------------------------------------------------------------------- /src/version.rs: -------------------------------------------------------------------------------- 1 | //! Information about the version of the EditorConfig specification this library complies with. 2 | //! 3 | //! The constants in this module specify the latest version of EditorConfig that ec4rs 4 | //! is known to be compliant with. 5 | //! Compliance is determined by running the `ec4rs_parse` tool 6 | //! against the same core test suite used by the reference implementation of EditorConfig. 7 | #![allow(missing_docs)] 8 | 9 | pub static STRING: &str = "0.17.2"; 10 | pub static MAJOR: usize = 0; 11 | pub static MINOR: usize = 17; 12 | pub static PATCH: usize = 2; 13 | -------------------------------------------------------------------------------- /.github/workflows/check-ctest.yml: -------------------------------------------------------------------------------- 1 | name: Check Compliance 2 | 3 | on: 4 | pull_request: 5 | paths: 6 | - '**.rs' 7 | - '.github/workflows/check-ctest.yml' 8 | push: 9 | branches: 10 | - main 11 | 12 | jobs: 13 | compliance: 14 | name: Check Compliance 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Checkout 18 | uses: actions/checkout@v5 19 | with: 20 | submodules: true 21 | - name: Install Rust 22 | run: "rustup toolchain install --profile minimal stable" 23 | - name: Build 24 | run: "cargo build -p ec4rs_tools" 25 | - name: Core Tests 26 | shell: bash 27 | run: | 28 | cd tests 29 | cmake -DEDITORCONFIG_CMD="$PWD/../target/debug/ec4rs-parse" . 30 | ctest . 31 | -------------------------------------------------------------------------------- /glob/src/parser/numrange.rs: -------------------------------------------------------------------------------- 1 | use super::Chars; 2 | 3 | fn parse_int(chars: &mut Chars<'_>, breaker: char) -> Option { 4 | let mut num = String::with_capacity(2); 5 | num.push(chars.next().filter(|c| c.is_numeric() || *c == '-')?); 6 | for c in chars { 7 | if c.is_numeric() { 8 | num.push(c) 9 | } else if c == breaker { 10 | return Some(num); 11 | } else { 12 | break; 13 | } 14 | } 15 | None 16 | } 17 | 18 | pub fn parse(mut chars: Chars<'_>) -> Option<(isize, isize, Chars<'_>)> { 19 | let num_a = parse_int(&mut chars, '.')?; 20 | if !matches!(chars.next(), Some('.')) { 21 | return None; 22 | } 23 | let num_b: String = parse_int(&mut chars, '}')?; 24 | Some((num_a.parse().ok()?, num_b.parse().ok()?, chars)) 25 | } 26 | -------------------------------------------------------------------------------- /glob/src/flatset.rs: -------------------------------------------------------------------------------- 1 | use std::{borrow::Borrow, collections::BTreeSet}; 2 | 3 | /// Very minimal Vec+binary search set. 4 | #[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Default, Debug)] 5 | pub struct FlatSet(Vec); 6 | 7 | impl FlatSet { 8 | pub fn as_slice(&self) -> &[T] { 9 | self.0.as_slice() 10 | } 11 | pub fn contains(&self, value: impl Borrow) -> bool { 12 | self.0.binary_search(value.borrow()).is_ok() 13 | } 14 | } 15 | 16 | impl From> for FlatSet { 17 | fn from(value: BTreeSet) -> Self { 18 | FlatSet(value.into_iter().collect()) 19 | } 20 | } 21 | 22 | impl From> for FlatSet { 23 | fn from(mut value: Vec) -> Self { 24 | value.sort_unstable(); 25 | value.dedup(); 26 | FlatSet(value) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /glob/README.md: -------------------------------------------------------------------------------- 1 | # ec4rs_glob 2 | 3 | The globbing engine used by [`ec4rs`](https://github.com/TheDaemoness/ec4rs). 4 | Please refer to that project for licensing and contribution info. 5 | 6 | You're probably better-served by using the 7 | [`glob` crate](https://crates.io/crates/glob) 8 | or the [`globset` crate](https://crates.io/crates/globset), 9 | both of which are widely-used and have good maintenance status at the time of 10 | writing. The only reason to use this crate is if you need: 11 | 12 | - Numeric range patterns (e.g. `{1..42}`), 13 | - An incredibly permissive glob engine, or 14 | - A glob engine that is more featureful than `glob` 15 | but lighter than `globset` (which pulls in parts of `regex`). 16 | 17 | ## Glob Features 18 | 19 | This crate supports the exact set of features necessary for 20 | perfect EditorConfig compliance, including passing the entire suite of 21 | glob tests for EditorConfig cores. Details can be found 22 | [on the EditorConfig specification](https://editorconfig.org/#wildcards). 23 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | # Cheat sheet: https://doc.rust-lang.org/cargo/reference/manifest.html 2 | 3 | [package] 4 | name = "ec4rs" 5 | description = "EditorConfig For Rust" 6 | license = "Apache-2.0" 7 | homepage = "https://github.com/TheDaemoness/ec4rs" 8 | repository = "https://github.com/TheDaemoness/ec4rs" 9 | readme = "README.md" 10 | keywords = ["editorconfig"] 11 | categories = ["config", "parser-implementations"] 12 | edition = "2021" 13 | 14 | authors = ["TheDaemoness"] 15 | include = ["/src", "/README.md", "/rustdoc.md"] 16 | rust-version = "1.56" # 2021 edition 17 | version = "1.2.0" 18 | 19 | [workspace] 20 | members = ["glob", "tools"] 21 | 22 | [features] 23 | default = ["ec4rs_glob"] 24 | track-source = [] 25 | language-tags = [] 26 | ec4rs_glob = ["dep:ec4rs_glob"] 27 | globset = ["dep:globset"] 28 | 29 | [dependencies] 30 | ec4rs_glob = { version = "0.1.0", path = "glob", optional = true } 31 | globset = { version = "0.4.16", optional = true, default-features = false } 32 | 33 | [package.metadata.docs.rs] 34 | all-features = true 35 | rustdoc-args = ["--cfg", "doc_unstable"] 36 | -------------------------------------------------------------------------------- /src/fallback.rs: -------------------------------------------------------------------------------- 1 | use crate::property as prop; 2 | 3 | pub fn add_fallbacks(props: &mut crate::Properties, legacy: bool) { 4 | let val = props.get_raw::(); 5 | if let Some(value) = val { 6 | if let Ok(prop::IndentSize::UseTabWidth) = value.parse::() { 7 | let value = props 8 | .get_raw::() 9 | .cloned() 10 | .unwrap_or(crate::string::SharedString::new_static("tab")); 11 | props.insert_raw::(value); 12 | } else { 13 | let value = value.to_owned(); 14 | let _ = props.try_insert_raw::(value); 15 | } 16 | } else if let Some(value) = props 17 | .get_raw::() 18 | .filter(|v| *v != &crate::string::UNSET) 19 | { 20 | let _ = props.try_insert_raw::(value.to_owned()); 21 | } 22 | if !legacy { 23 | if let Ok(prop::IndentStyle::Tabs) = props.get::() { 24 | let _ = props.try_insert(prop::IndentSize::UseTabWidth); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/properties/iter.rs: -------------------------------------------------------------------------------- 1 | #![allow(unused_imports)] 2 | use crate::properties::Properties; 3 | use crate::string::SharedString; 4 | 5 | macro_rules! impls { 6 | ($name:ident, $valuetype:ty) => { 7 | impl<'a> Iterator for $name<'a> { 8 | type Item = (&'a str, $valuetype); 9 | fn next(&mut self) -> Option { 10 | let pair = self.0.next()?; 11 | let (ref key, val) = pair; 12 | Some((key, val)) 13 | } 14 | 15 | fn size_hint(&self) -> (usize, Option) { 16 | self.0.size_hint() 17 | } 18 | } 19 | impl<'a> DoubleEndedIterator for $name<'a> { 20 | fn next_back(&mut self) -> Option { 21 | let pair = self.0.next_back()?; 22 | let (ref key, val) = pair; 23 | Some((key, val)) 24 | } 25 | } 26 | impl<'a> std::iter::FusedIterator for $name<'a> {} 27 | //TODO: PartialEq/Eq? 28 | }; 29 | } 30 | 31 | /// An iterator over [`Properties`]. 32 | #[derive(Clone)] 33 | pub struct Iter<'a>(pub(super) std::slice::Iter<'a, (SharedString, SharedString)>); 34 | 35 | impls! {Iter, &'a SharedString} 36 | 37 | /// An iterator over [`Properties`] that allows value mutation. 38 | pub struct IterMut<'a>(pub(super) std::slice::IterMut<'a, (SharedString, SharedString)>); 39 | 40 | impls! {IterMut, &'a mut SharedString} 41 | -------------------------------------------------------------------------------- /DCO.txt: -------------------------------------------------------------------------------- 1 | Developer Certificate of Origin 2 | Version 1.1 3 | 4 | Copyright (C) 2004, 2006 The Linux Foundation and its contributors. 5 | 6 | Everyone is permitted to copy and distribute verbatim copies of this 7 | license document, but changing it is not allowed. 8 | 9 | 10 | Developer's Certificate of Origin 1.1 11 | 12 | By making a contribution to this project, I certify that: 13 | 14 | (a) The contribution was created in whole or in part by me and I 15 | have the right to submit it under the open source license 16 | indicated in the file; or 17 | 18 | (b) The contribution is based upon previous work that, to the best 19 | of my knowledge, is covered under an appropriate open source 20 | license and I have the right under that license to submit that 21 | work with modifications, whether created in whole or in part 22 | by me, under the same open source license (unless I am 23 | permitted to submit under a different license), as indicated 24 | in the file; or 25 | 26 | (c) The contribution was provided directly to me by some other 27 | person who certified (a), (b) or (c) and I have not modified 28 | it. 29 | 30 | (d) I understand and agree that this project and the contribution 31 | are public and that a record of the contribution (including all 32 | personal information I submit with it, including my sign-off) is 33 | maintained indefinitely and may be redistributed consistent with 34 | this project or the open source license(s) involved. 35 | -------------------------------------------------------------------------------- /.github/workflows/check-glob.yml: -------------------------------------------------------------------------------- 1 | name: Check ec4rs_glob 2 | 3 | on: 4 | pull_request: 5 | paths: 6 | - 'glob/**.rs' 7 | - 'glob/Cargo.toml' 8 | - '.github/workflows/check-glob.yml' 9 | push: 10 | branches: 11 | - main 12 | 13 | jobs: 14 | test: 15 | name: MSRV Tests (glob) 16 | runs-on: ${{ matrix.os }} 17 | strategy: 18 | matrix: 19 | build: [linux, windows] 20 | include: 21 | - build: linux 22 | os: ubuntu-latest 23 | - build: windows 24 | os: windows-latest 25 | steps: 26 | - name: Checkout 27 | uses: actions/checkout@v5 28 | - name: Install MSRV Rust 29 | run: "rustup toolchain install --profile minimal 1.56" 30 | - name: Build (glob) 31 | run: "cargo build -p ec4rs_glob" 32 | - name: Unit Tests (glob) 33 | run: "cargo test -p ec4rs_glob" 34 | quality: 35 | name: Check Quality (glob) 36 | runs-on: ubuntu-latest 37 | steps: 38 | - name: Checkout 39 | uses: actions/checkout@v5 40 | - name: Install Rust 41 | run: | 42 | rustup toolchain install --profile minimal 43 | rustup component add clippy 44 | rustup component add rustfmt 45 | - name: Check Clippy 46 | run: "cargo clippy -p ec4rs_glob" 47 | - name: Check Docs 48 | run: "cargo doc --no-deps -p ec4rs_glob" 49 | - name: Check Style 50 | run: "cargo fmt --check -p ec4rs_glob" 51 | -------------------------------------------------------------------------------- /.github/workflows/check-lib.yml: -------------------------------------------------------------------------------- 1 | name: Check ec4rs 2 | 3 | on: 4 | pull_request: 5 | paths: 6 | - 'src/**.rs' 7 | - 'Cargo.toml' 8 | - '.github/workflows/check-lib.yml' 9 | push: 10 | branches: 11 | - main 12 | 13 | jobs: 14 | test: 15 | name: MSRV Tests (library) 16 | runs-on: ${{ matrix.os }} 17 | strategy: 18 | matrix: 19 | build: [linux, windows] 20 | include: 21 | - build: linux 22 | os: ubuntu-latest 23 | - build: windows 24 | os: windows-latest 25 | steps: 26 | - name: Checkout 27 | uses: actions/checkout@v5 28 | - name: Install MSRV Rust 29 | run: "rustup toolchain install --profile minimal 1.56" 30 | - name: Build (library) 31 | run: "cargo build -p ec4rs" 32 | - name: Unit Tests (library) 33 | run: "cargo test -p ec4rs" 34 | - name: Unit Tests (library, all-features) 35 | run: "cargo test -p ec4rs --all-features" 36 | quality: 37 | name: Code Quality (library) 38 | runs-on: ubuntu-latest 39 | steps: 40 | - name: Checkout 41 | uses: actions/checkout@v5 42 | - name: Install Rust 43 | run: | 44 | rustup toolchain install --profile minimal 45 | rustup component add clippy 46 | rustup component add rustfmt 47 | - name: Check Clippy 48 | run: "cargo clippy -p ec4rs" 49 | - name: Check Docs 50 | run: "cargo doc --no-deps -p ec4rs" 51 | - name: Check Style 52 | run: "cargo fmt --check -p ec4rs" 53 | -------------------------------------------------------------------------------- /src/tests/property.rs: -------------------------------------------------------------------------------- 1 | use crate::PropertyKey; 2 | 3 | #[test] 4 | fn standard_keys_matches() { 5 | use crate::property::*; 6 | macro_rules! contained { 7 | ($prop:ident) => { 8 | assert!( 9 | STANDARD_KEYS.contains(&$prop::key()), 10 | "STANDARD_KEYS is missing {}", 11 | $prop::key() 12 | ) 13 | }; 14 | } 15 | contained!(IndentStyle); 16 | contained!(IndentSize); 17 | contained!(TabWidth); 18 | contained!(EndOfLine); 19 | contained!(Charset); 20 | contained!(TrimTrailingWs); 21 | contained!(FinalNewline); 22 | contained!(SpellingLanguage); 23 | assert!(!STANDARD_KEYS.contains(&MaxLineLen::key())); // Not MaxLineLen 24 | } 25 | 26 | #[test] 27 | fn spelling_language() { 28 | use crate::property::SpellingLanguage; 29 | use crate::string::SharedString; 30 | use crate::PropertyValue; 31 | // This is more testing language-tags than anything, 32 | // but for language-tags to be useful here, 33 | let testcase_en = SharedString::new_static("en"); 34 | let parsed = match SpellingLanguage::from_shared_string(&testcase_en) { 35 | Ok(SpellingLanguage::Value(v)) => v, 36 | e => { 37 | let v = e.expect("parsing should succeed"); 38 | panic!("unexpected value {v:?}"); 39 | } 40 | }; 41 | assert_eq!(parsed.primary_language(), &*testcase_en); 42 | let testcase_en_us = SharedString::new_static("en-US"); 43 | let parsed = match SpellingLanguage::from_shared_string(&testcase_en_us) { 44 | Ok(SpellingLanguage::Value(v)) => v, 45 | e => { 46 | let v = e.expect("parsing should succeed"); 47 | panic!("unexpected value {v:?}"); 48 | } 49 | }; 50 | assert_eq!(parsed.to_string(), &*testcase_en_us); 51 | } 52 | -------------------------------------------------------------------------------- /src/traits.rs: -------------------------------------------------------------------------------- 1 | use crate::string::SharedString; 2 | 3 | /// Trait for types that be converted to [`SharedString`]s 4 | /// and parsed back out of the returned [`SharedString`]s. 5 | pub trait PropertyValue: 6 | Sized + std::str::FromStr + crate::string::ToSharedString + Default 7 | { 8 | /// Parses a value from a [`SharedString`]. 9 | /// 10 | /// Some types may contain a copy of the string they were parsed from. 11 | /// This function allows an more-efficient implementation. 12 | /// 13 | /// For consistency reasons, if `self` contains a `SharedString`, 14 | /// it should not have a source. 15 | /// See [`SharedString::clear_source`]. 16 | fn from_shared_string(value: &SharedString) -> Result { 17 | std::str::FromStr::from_str(value) 18 | } 19 | } 20 | 21 | /// Trait for types that are associated with property names. 22 | /// 23 | /// Types that implement this trait will usually also implement [`PropertyValue`]. 24 | pub trait PropertyKey { 25 | /// The lowercase string key for this property. 26 | /// 27 | /// Used to look up the value in a [`crate::Properties`] map. 28 | fn key() -> &'static str; 29 | } 30 | 31 | /// Tests if the result of parsing the result of a `ToSharedString` conversion 32 | /// is *not unequal* to the original value. 33 | /// 34 | /// # Panics 35 | /// Panics if the initial and result values are not equal. 36 | #[cfg(test)] 37 | pub fn test_reparse(initial: &T) 38 | where 39 | T: Clone + PropertyValue + std::fmt::Debug + PartialEq, 40 | { 41 | let written: SharedString = initial.clone().to_shared_string(); 42 | let result = T::from_shared_string(&written).expect("reparse errored"); 43 | assert!( 44 | !result.ne(initial), 45 | "reparsed value is unequal to original; expected `{:?}`, got `{:?}`", 46 | initial, 47 | result 48 | ) 49 | } 50 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 1.x 2 | 3 | ## 1.2.0 (2025-04-19) 4 | 5 | - Added feature `track-source` to track where any given value came from. 6 | - Added `-0Hl` flags to `ec4rs-parse` for displaying value sources. 7 | - Added `RawValue::to_lowercase`. 8 | - Implemented `Display` for `RawValue`. 9 | - Changed `ec4rs-parse` to support empty values for compliance with 10 | EditorConfig `0.17.2`. 11 | - Fixed fallbacks adding an empty value for `indent_size`. 12 | - Fixed `Properties::iter` and `Properties::iter_mut` not returning 13 | pairs with empty values when `allow-empty-values` is enabled. 14 | 15 | ## 1.1.1 (2024-08-29) 16 | 17 | - Update testing instructions to work with the latest versions of cmake+ctest. 18 | - Fix `/*` matching too broadly (#12). 19 | 20 | ## 1.1.0 (2024-03-26) 21 | 22 | - Added optional `spelling_language` parsing for EditorConfig `0.16.0`. 23 | This adds an optional dependency on the widely-used `language-tags` crate 24 | to parse a useful superset of the values allowed by the spec. 25 | - Added feature `allow-empty-values` to allow empty key-value pairs (#7). 26 | Added to opt-in to behavioral breakage with `1.0.x`; a future major release 27 | will remove this feature and make its functionality the default. 28 | - Implemented more traits for `Properties`. 29 | - Changed `LineReader` to allow comments after section headers (#6). 30 | - Slightly optimized glob performance. 31 | 32 | Thanks to @kyle-rader-msft for contributing parser improvements! 33 | 34 | ## 1.0.2 (2023-03-23) 35 | 36 | - Updated the test suite to demonstrate compliance with EditorConfig `0.15.1`. 37 | - Fixed inconsistent character class behavior when 38 | the character class does not end with `]`. 39 | - Fixed redundant UTF-8 validity checks when globbing. 40 | - Reorganized parts of the `glob` module to greatly improve code quality. 41 | 42 | ## 1.0.1 (2022-06-24) 43 | 44 | - Reduced the MSRV for `ec4rs` to `1.56`, from `1.59`. 45 | 46 | ## 1.0.0 (2022-06-11) 47 | 48 | Initial stable release! 49 | -------------------------------------------------------------------------------- /src/glob.rs: -------------------------------------------------------------------------------- 1 | //! Glob engine abstractions. 2 | //! 3 | //! If the `ec4rs_glob` feature is enabled, 4 | //! this module also includes a re-export of `ec4rs_glob`. 5 | 6 | /// A parsed glob pattern which can be used for matching paths. 7 | /// 8 | /// This is intended to expose only the subset of functionality relevant for parsing 9 | /// EditorConfig files, and therefore does not include any way to configure a builder 10 | /// for the glob pattern. 11 | pub trait Pattern { 12 | /// The type of error returned by a failed parse. 13 | type Error: std::error::Error + Sync + Send + 'static; 14 | /// Attempts to parse `Self` out of a string. 15 | fn parse(pattern: &str) -> Result 16 | where 17 | Self: Sized; 18 | /// Returns `true` if the provided path matches `Self`. 19 | /// 20 | /// If evaluation errors, such as due to depth limits being reached, 21 | /// this function must return `false`. 22 | #[must_use] 23 | fn matches(&self, path: &std::path::Path) -> bool; 24 | } 25 | 26 | #[cfg(feature = "ec4rs_glob")] 27 | pub use ec4rs_glob::*; 28 | 29 | #[cfg(feature = "ec4rs_glob")] 30 | impl Pattern for Glob { 31 | type Error = std::convert::Infallible; 32 | 33 | fn parse(pattern: &str) -> Result 34 | where 35 | Self: Sized, 36 | { 37 | // TODO: Size and depth limits. 38 | Ok(Glob::new(pattern)) 39 | } 40 | 41 | fn matches(&self, path: &std::path::Path) -> bool { 42 | self.matches(path) 43 | } 44 | } 45 | 46 | #[cfg(feature = "globset")] 47 | impl Pattern for globset::GlobMatcher { 48 | type Error = globset::Error; 49 | 50 | fn parse(pattern: &str) -> Result 51 | where 52 | Self: Sized, 53 | { 54 | let glob = globset::Glob::new(pattern)?; 55 | Ok(glob.compile_matcher()) 56 | } 57 | 58 | fn matches(&self, path: &std::path::Path) -> bool { 59 | self.is_match(path) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | /// Possible errors that can occur while parsing EditorConfig data. 2 | #[derive(Debug)] 3 | pub enum ParseError { 4 | /// End-of-file was reached. 5 | Eof, 6 | /// An IO read failure occurred. 7 | Io(std::io::Error), 8 | /// An invalid line was read. 9 | InvalidLine, 10 | } 11 | 12 | impl std::fmt::Display for ParseError { 13 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 14 | match self { 15 | ParseError::Eof => write!(f, "end of data"), 16 | ParseError::Io(e) => write!(f, "io failure: {}", e), 17 | ParseError::InvalidLine => write!(f, "invalid line"), 18 | } 19 | } 20 | } 21 | 22 | impl std::error::Error for ParseError { 23 | fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { 24 | match self { 25 | ParseError::Io(e) => Some(e), 26 | _ => None, 27 | } 28 | } 29 | } 30 | 31 | /// All errors that can occur during operation. 32 | #[derive(Debug)] 33 | pub enum Error { 34 | /// An error occured durign parsing. 35 | Parse(ParseError), 36 | /// An error occured during parsing of a file. 37 | InFile(std::path::PathBuf, usize, ParseError), 38 | /// The current working directory is invalid (e.g. does not exist). 39 | InvalidCwd(std::io::Error), 40 | } 41 | 42 | impl std::fmt::Display for Error { 43 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 44 | match self { 45 | Error::Parse(error) => write!(f, "{}", error), 46 | Error::InFile(path, line, error) => { 47 | write!(f, "{}:{}: {}", path.to_string_lossy(), line, error) 48 | } 49 | Error::InvalidCwd(ioe) => write!(f, "invalid cwd: {}", ioe), 50 | } 51 | } 52 | } 53 | 54 | impl std::error::Error for Error { 55 | fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { 56 | match self { 57 | Error::Parse(pe) | Error::InFile(_, _, pe) => pe.source(), 58 | Error::InvalidCwd(ioe) => Some(ioe), 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/tests/linereader.rs: -------------------------------------------------------------------------------- 1 | use crate::linereader::*; 2 | use crate::ParseError; 3 | 4 | fn test_lines(lines: &[(&'static str, Line<'static>)]) { 5 | for (line, expected) in lines { 6 | assert_eq!(parse_line(line).unwrap(), *expected) 7 | } 8 | } 9 | 10 | #[test] 11 | fn valid_props() { 12 | use Line::Pair; 13 | test_lines(&[ 14 | ("foo=bar", Pair("foo", "bar")), 15 | ("Foo=Bar", Pair("Foo", "Bar")), 16 | ("foo = bar", Pair("foo", "bar")), 17 | (" foo = bar ", Pair("foo", "bar")), 18 | ("foo=bar=baz", Pair("foo", "bar=baz")), 19 | (" foo = bar = baz ", Pair("foo", "bar = baz")), 20 | ("foo = bar #baz", Pair("foo", "bar #baz")), 21 | ("foo = [bar]", Pair("foo", "[bar]")), 22 | ("foo =", Pair("foo", "")), 23 | ("foo = ", Pair("foo", "")), 24 | ]) 25 | } 26 | 27 | #[test] 28 | fn valid_sections() { 29 | use Line::Section; 30 | test_lines(&[ 31 | ("[foo]", Section("foo")), 32 | ("[[foo]]", Section("[foo]")), 33 | ("[ foo ]", Section(" foo ")), 34 | ("[][]]", Section("][]")), 35 | ("[Foo]", Section("Foo")), 36 | (" [foo] ", Section("foo")), 37 | ("[a=b]", Section("a=b")), 38 | ("[#foo]", Section("#foo")), 39 | ("[foo] #comment", Section("foo")), 40 | ("[foo] ;comment", Section("foo")), 41 | ]) 42 | } 43 | 44 | #[test] 45 | fn valid_nothing() { 46 | use Line::Nothing; 47 | test_lines(&[ 48 | ("\t", Nothing), 49 | ("\r", Nothing), 50 | ("", Nothing), 51 | (" ", Nothing), 52 | (";comment", Nothing), 53 | ("#comment", Nothing), 54 | (" # comment", Nothing), 55 | ("# [section]", Nothing), 56 | ("# foo=bar", Nothing), 57 | ]) 58 | } 59 | 60 | #[test] 61 | fn invalid() { 62 | let lines = [ 63 | "[]", 64 | "[close", 65 | "open]", 66 | "][", 67 | "nonproperty", 68 | "=", 69 | " = nokey", 70 | ]; 71 | for line in lines { 72 | assert!(matches!( 73 | parse_line(line).unwrap_err(), 74 | ParseError::InvalidLine 75 | )) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /glob/src/matcher.rs: -------------------------------------------------------------------------------- 1 | use super::{Glob, Splitter}; 2 | 3 | #[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug)] 4 | pub enum Matcher { 5 | End, 6 | AnySeq(bool), 7 | AnyChar, 8 | Sep, 9 | Suffix(String), 10 | // TODO: Grapheme clusters? 11 | CharClass(super::FlatSet, bool), 12 | Range(isize, isize), 13 | Any(super::FlatSet), 14 | } 15 | 16 | fn try_match<'a, 'b>( 17 | splitter: Splitter<'a>, 18 | matcher: &'b Matcher, 19 | state: &mut super::stack::SaveStack<'a, 'b>, 20 | ) -> Option> { 21 | match matcher { 22 | Matcher::End => splitter.match_end(), 23 | Matcher::Sep => splitter.match_sep(), 24 | Matcher::AnyChar => splitter.match_any(false), 25 | Matcher::AnySeq(sep) => { 26 | if let Some(splitter) = splitter.clone().match_any(*sep) { 27 | state.add_rewind(splitter, matcher); 28 | } 29 | Some(splitter) 30 | } 31 | Matcher::Suffix(s) => splitter.match_suffix(s.as_str()), 32 | Matcher::CharClass(cs, should_have) => { 33 | let (splitter, c) = splitter.next_char()?; 34 | if cs.contains(c) != *should_have { 35 | return None; 36 | } 37 | Some(splitter) 38 | } 39 | Matcher::Range(lower, upper) => splitter.match_number(*lower, *upper), 40 | Matcher::Any(options) => { 41 | state.add_alts(splitter.clone(), options.as_slice()); 42 | Some(splitter) 43 | } 44 | } 45 | } 46 | 47 | #[must_use] 48 | pub fn matches<'a>(path: &'a std::path::Path, glob: &Glob) -> Option> { 49 | let mut splitter = super::Splitter::new(path)?; 50 | let mut state = super::stack::SaveStack::new(&splitter, glob); 51 | loop { 52 | if let Some(matcher) = state.globs().next() { 53 | if let Some(splitter_new) = try_match(splitter, matcher, &mut state) { 54 | splitter = splitter_new; 55 | } else if let Some(splitter_new) = state.restore() { 56 | splitter = splitter_new; 57 | } else { 58 | return None; 59 | } 60 | } else { 61 | return Some(splitter); 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /rustdoc.md: -------------------------------------------------------------------------------- 1 | # ec4rs: EditorConfig For Rust 2 | 3 | An 4 | [EditorConfig](https://editorconfig.org/) 5 | [core](https://editorconfig-specification.readthedocs.io/#terminology) 6 | in safe Rust. 7 | See [the Github repo](https://github.com/TheDaemoness/ec4rs) 8 | for more information. 9 | 10 | ## Basic Example Usage 11 | 12 | The most common usecase for `ec4rs` involves 13 | determining how an editor/linter/etc should be configured 14 | for a file at a given path. 15 | 16 | The simplest way to load these is using [`properties_of`]. 17 | This function, if successful, will return a [`Properties`], 18 | a map of config keys to values for a file at the provided path. 19 | In order to get values for tab width and indent size that are compliant 20 | with the standard, [`use_fallbacks`][Properties::use_fallbacks] 21 | should be called before retrieving them. 22 | 23 | From there, `Properties` offers several methods for retrieving values: 24 | 25 | ``` 26 | // Read the EditorConfig files that would apply to a file at the given path. 27 | let mut cfg = ec4rs::properties_of::("src/main.rs") 28 | .unwrap_or_default(); 29 | // Convenient access to ec4rs's property parsers. 30 | use ec4rs::property::*; 31 | // Use fallback values for tab width and/or indent size. 32 | cfg.use_fallbacks(); 33 | 34 | // Let ec4rs do the parsing for you. 35 | let indent_style: IndentStyle = cfg.get::() 36 | .unwrap_or(IndentStyle::Tabs); 37 | 38 | // Get a string value, with a default. 39 | // ec4rs has a string type designed for immutability and minimal allocations. 40 | let charset = cfg.get_raw::() 41 | .cloned() 42 | .unwrap_or(ec4rs::string::SharedString::new_static("utf-8")); 43 | 44 | // Parse a non-standard property. 45 | let hard_wrap = cfg.get_raw_for_key("max_line_length") 46 | .unwrap_or_default() 47 | .parse::(); 48 | ``` 49 | 50 | ## Features 51 | 52 | `ec4rs_glob` (Default): 53 | Enable support for an EditorConfig-compliant glob implementation. 54 | 55 | `globset`: 56 | Add support for [`globset`](https://docs.rs/globset/latest/globset/) 57 | as an alternative glob implementation. 58 | Note that `globset` patterns do not conform to the EditorConfig standard, 59 | but it should be good enough for most real-world cases. 60 | 61 | `language-tags`: NYI for 2.0. 62 | 63 | `track-source`: Allow [`SharedString`][crate::string::SharedString] 64 | to store the file and line number it originates from. 65 | [`ConfigParser`] will add this information where applicable. 66 | -------------------------------------------------------------------------------- /glob/src/parser/charclass.rs: -------------------------------------------------------------------------------- 1 | use std::collections::BTreeSet; 2 | 3 | use super::Chars; 4 | use crate::{Glob, Matcher}; 5 | 6 | #[inline] 7 | fn grow_char_class(chars: &mut Chars<'_>, charclass: &mut BTreeSet) -> Option<()> { 8 | // Previous character. 9 | let mut pc = '['; 10 | let mut not_at_start = false; 11 | loop { 12 | match chars.next()? { 13 | ']' => return Some(()), 14 | '\\' => { 15 | pc = chars.next()?; 16 | charclass.insert(pc); 17 | } 18 | // The spec says nothing about char ranges, 19 | // but the test suite tests for them. 20 | // Therefore, EC has them in practice. 21 | '-' if not_at_start => { 22 | let nc = match chars.next()? { 23 | ']' => { 24 | charclass.insert('-'); 25 | return Some(()); 26 | } 27 | '\\' => chars.next()?, 28 | other => other, 29 | }; 30 | charclass.extend(pc..=nc); 31 | pc = nc; 32 | } 33 | c => { 34 | charclass.insert(c); 35 | pc = c; 36 | } 37 | } 38 | not_at_start = true; 39 | } 40 | } 41 | 42 | pub fn parse(mut glob: Glob, mut chars: Chars<'_>) -> (Glob, Chars<'_>) { 43 | let invert = if let Some(c) = chars.peek() { 44 | *c == '!' 45 | } else { 46 | glob.append_escaped('['); 47 | return (glob, chars); 48 | }; 49 | let restore = chars.clone(); 50 | if invert { 51 | chars.next(); 52 | } 53 | let mut charclass = BTreeSet::::new(); 54 | if grow_char_class(&mut chars, &mut charclass).is_some() { 55 | // Remove slashes for the sake of consistent behavior. 56 | charclass.remove(&'/'); 57 | match charclass.len() { 58 | 0 => { 59 | if invert { 60 | glob.push(Matcher::AnyChar); 61 | } else { 62 | glob.append_escaped('['); 63 | glob.append_escaped(']'); 64 | } 65 | } 66 | // Don't use BTreeSet::first here (stable: 1.66). 67 | 1 => glob.append_escaped(*charclass.iter().next().unwrap()), 68 | _ => glob.push(Matcher::CharClass(charclass.into(), !invert)), 69 | } 70 | (glob, chars) 71 | } else { 72 | glob.append_escaped('['); 73 | (glob, restore) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/property/language_tag.rs: -------------------------------------------------------------------------------- 1 | use super::UnknownValueError; 2 | 3 | // TODO: Use std::ascii::Char when it stabilizes. 4 | 5 | /// The subset of BCP 47 language tags permitted by the EditorConfig standard. 6 | /// 7 | /// This type doesn't implement [`PropertyValue`][crate::PropertyValue]. 8 | /// See [`SpellingLanguage`][super::SpellingLanguage]. 9 | #[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] 10 | pub struct LanguageTag([u8; 4]); 11 | 12 | impl LanguageTag { 13 | /// Attempt to parse `self` from the provided string. 14 | pub fn try_from(string: impl AsRef) -> Result { 15 | match string.as_ref().as_bytes() { 16 | [a, b] => (a.is_ascii_alphabetic() && b.is_ascii_alphabetic()) 17 | .then(|| Self([a.to_ascii_lowercase(), b.to_ascii_lowercase(), 0, 0])), 18 | [a, b, b'-', c, d] => (a.is_ascii_alphabetic() 19 | && b.is_ascii_alphabetic() 20 | && c.is_ascii_alphabetic() 21 | && d.is_ascii_alphabetic()) 22 | .then(|| { 23 | Self([ 24 | a.to_ascii_lowercase(), 25 | b.to_ascii_lowercase(), 26 | c.to_ascii_uppercase(), 27 | d.to_ascii_uppercase(), 28 | ]) 29 | }), 30 | _ => None, 31 | } 32 | .ok_or(UnknownValueError) 33 | } 34 | /// Returns the language subtag, e.g. the "en" in "en-US". 35 | pub fn primary_language(&self) -> &str { 36 | #[allow(clippy::missing_panics_doc)] 37 | std::str::from_utf8(&self.0[0..2]).expect("Non-UTF-8 bytes in LanguageTag") 38 | } 39 | /// Returns the region subtag, if any, e.g. the "US" in "en-US". 40 | pub fn region(&self) -> Option<&str> { 41 | let slice = &self.0[2..4]; 42 | #[allow(clippy::missing_panics_doc)] 43 | (*slice != [0, 0]) 44 | .then(|| std::str::from_utf8(slice).expect("Non-UTF-8 bytes in LanguageTag")) 45 | } 46 | } 47 | 48 | impl std::fmt::Display for LanguageTag { 49 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 50 | if let Some(region) = self.region() { 51 | write!(f, "{}-{}", self.primary_language(), region) 52 | } else { 53 | write!(f, "{}", self.primary_language()) 54 | } 55 | } 56 | } 57 | 58 | impl std::str::FromStr for LanguageTag { 59 | type Err = UnknownValueError; 60 | 61 | fn from_str(s: &str) -> Result { 62 | Self::try_from(s) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ec4rs: EditorConfig For Rust 2 | [![Check ec4rs](https://github.com/TheDaemoness/ec4rs/actions/workflows/check-lib.yml/badge.svg)](https://github.com/TheDaemoness/ec4rs/actions/workflows/check-lib.yml) 3 | [![crates.io](https://img.shields.io/crates/v/ec4rs.svg)](https://crates.io/crates/ec4rs) 4 | [![API docs](https://docs.rs/ec4rs/badge.svg)](https://docs.rs/ec4rs) 5 | 6 | An 7 | [EditorConfig](https://editorconfig.org/) 8 | [core](https://editorconfig-specification.readthedocs.io/#terminology) 9 | in safe Rust. 10 | 11 | This library enables you to integrate EditorConfig support 12 | into any tools which may benefit from it, 13 | such as code editors, formatters, and style linters. 14 | It includes mechanisms for type-safe parsing of properties, 15 | so that your tool doesn't have to do it itself. 16 | It also exposes significant portions of its logic, 17 | allowing you to use only the parts you need. 18 | 19 | Name idea shamelessly stolen from [ec4j](https://github.com/ec4j/ec4j). 20 | This library has minimal dependencies (only `std` at this time). 21 | 22 | For example usage, see [the docs](https://docs.rs/ec4rs). 23 | 24 | ## Testing 25 | 26 | [![Check Compliance](https://github.com/TheDaemoness/ec4rs/actions/workflows/check-ctest.yml/badge.svg)](https://github.com/TheDaemoness/ec4rs/actions/workflows/check-ctest.yml) 27 | 28 | The main repository for this library includes the EditorConfig 29 | [core tests](https://github.com/editorconfig/editorconfig-core-test) 30 | as a Git submodule. This library should pass all of these tests. 31 | To run the test suite, run the following commands in a POSIX-like shell: 32 | 33 | ```bash 34 | cargo build --package ec4rs_tools 35 | git submodule update --init --recursive 36 | cd tests 37 | cmake -DEDITORCONFIG_CMD="$PWD/../target/debug/ec4rs-parse" . 38 | ctest . 39 | ``` 40 | 41 | ## Glob Library 42 | 43 | [![Check ec4rs_glob](https://github.com/TheDaemoness/ec4rs/actions/workflows/check-glob.yml/badge.svg)](https://github.com/TheDaemoness/ec4rs/actions/workflows/check-glob.yml) 44 | 45 | This repository also includes the [`ec4rs_glob`](/glob) library which is 46 | primarily intended for internal use but can be used by other projects. See 47 | [its README](/glob/README.md) for more information. 48 | 49 | ## License 50 | 51 | `ec4rs`, `ec4rs_glob`, and `ec4rs_tools` are licensed under the 52 | [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0.html) 53 | with no `NOTICE` text. 54 | 55 | Contributors submitting code changes must agree to the terms of the 56 | [Developer Certificate of Origin (DCO)](https://developercertificate.org/) 57 | to have their contributions accepted for inclusion. 58 | A copy of the DCO may be found in `DCO.txt`. 59 | Contributors should sign-off on their commits (see `git commit -s`) 60 | to indicate explicit agreement. 61 | -------------------------------------------------------------------------------- /glob/src/parser/main.rs: -------------------------------------------------------------------------------- 1 | use super::alt::AltStack; 2 | use crate::{Glob, Matcher}; 3 | 4 | pub fn parse(glob: &str) -> Glob { 5 | let mut retval = Glob(vec![]); 6 | let mut stack = AltStack::new(); 7 | for segment in glob.split('/') { 8 | retval.append_escaped('/'); 9 | let mut chars = segment.chars().peekable(); 10 | while let Some(c) = chars.next() { 11 | match c { 12 | '\\' => { 13 | if let Some(escaped) = chars.next() { 14 | retval.append_escaped(escaped); 15 | } 16 | } 17 | '?' => retval.push(Matcher::AnyChar), 18 | '*' => retval.push(Matcher::AnySeq(matches!(chars.peek(), Some('*')))), 19 | '[' => { 20 | let (retval_n, chars_n) = super::charclass::parse(retval, chars); 21 | retval = retval_n; 22 | chars = chars_n; 23 | } 24 | '{' => { 25 | if let Some((a, b, chars_new)) = super::numrange::parse(chars.clone()) { 26 | chars = chars_new; 27 | retval.push(Matcher::Range( 28 | // Reading the spec strictly, 29 | // a compliant implementation must handle cases where 30 | // the left integer is greater than the right integer. 31 | std::cmp::min(a, b), 32 | std::cmp::max(a, b), 33 | )); 34 | } else { 35 | stack.push(retval); 36 | retval = Glob(vec![]); 37 | } 38 | } 39 | ',' => { 40 | if let Some(rejected) = stack.add_alt(retval) { 41 | retval = rejected; 42 | retval.append_escaped(','); 43 | } else { 44 | retval = Glob(vec![]); 45 | } 46 | } 47 | '}' => { 48 | let (retval_n, add_brace) = stack.add_alt_and_pop(retval); 49 | retval = retval_n; 50 | if add_brace { 51 | retval.append_escaped('}'); 52 | } 53 | } 54 | _ => retval.append_escaped(c), 55 | } 56 | } 57 | } 58 | loop { 59 | let (retval_n, is_empty) = stack.join_and_pop(retval); 60 | retval = retval_n; 61 | if is_empty { 62 | break; 63 | } 64 | } 65 | if glob.contains("/") { 66 | *retval.0.first_mut().unwrap() = Matcher::End; 67 | } 68 | if let Some(Matcher::Sep) = retval.0.last() { 69 | retval.push(Matcher::AnySeq(false)); 70 | } 71 | retval 72 | } 73 | -------------------------------------------------------------------------------- /src/tests/properties.rs: -------------------------------------------------------------------------------- 1 | use crate::{string::SharedString, Properties, PropertiesSource}; 2 | 3 | static BASIC_KEYS: [&str; 4] = ["2", "3", "0", "1"]; 4 | static ALT_VALUES: [&str; 4] = ["a", "b", "c", "d"]; 5 | 6 | fn zip_self() -> impl Iterator { 7 | BASIC_KEYS.iter().cloned().zip(BASIC_KEYS.iter().cloned()) 8 | } 9 | 10 | fn zip_alts() -> impl Iterator { 11 | BASIC_KEYS.iter().cloned().zip(ALT_VALUES.iter().cloned()) 12 | } 13 | 14 | fn test_basic_keys(props: &Properties) { 15 | for s in BASIC_KEYS { 16 | // Test mapping correctness using get. 17 | assert_eq!( 18 | props.get_raw_for_key(s).cloned(), 19 | Some(SharedString::new_static(s)) 20 | ) 21 | } 22 | // Ensure that they keys are returned in order. 23 | assert!(props.iter().map(|k| k.0).eq(BASIC_KEYS.iter().cloned())) 24 | } 25 | 26 | #[test] 27 | fn from_iter() { 28 | let props: Properties = zip_self().collect(); 29 | test_basic_keys(&props); 30 | } 31 | 32 | #[test] 33 | fn insert() { 34 | let mut props = Properties::new(); 35 | for s in BASIC_KEYS { 36 | props.insert_raw_for_key(s, s); 37 | } 38 | test_basic_keys(&props); 39 | } 40 | 41 | #[test] 42 | fn insert_replacing() { 43 | let mut props: Properties = zip_alts().collect(); 44 | for (k, v) in zip_alts() { 45 | let old = props.get_raw_for_key(k).expect("missing pair"); 46 | assert_eq!(old.as_str(), v); 47 | props.insert_raw_for_key(k, k); 48 | } 49 | test_basic_keys(&props); 50 | } 51 | 52 | #[test] 53 | fn try_insert() { 54 | let mut props = Properties::new(); 55 | for s in BASIC_KEYS { 56 | assert!(props.try_insert_raw_for_key(s, s).is_ok()); 57 | } 58 | test_basic_keys(&props); 59 | } 60 | 61 | #[test] 62 | fn try_insert_replacing() { 63 | let mut props: Properties = zip_self().collect(); 64 | for (k, v) in zip_alts() { 65 | assert_eq!( 66 | props 67 | .try_insert_raw_for_key(k, k) 68 | .expect_err("try_insert wrongly returns Ok for same value") 69 | .as_str(), 70 | k 71 | ); 72 | assert_eq!( 73 | props 74 | .try_insert_raw_for_key(k, v) 75 | .expect_err("try_insert wrongly returns Ok for update") 76 | .as_str(), 77 | k 78 | ); 79 | } 80 | } 81 | 82 | #[test] 83 | fn apply_empty_to() { 84 | let mut props = Properties::new(); 85 | props.insert_raw_for_key("foo", "a"); 86 | props.insert_raw_for_key("bar", "b"); 87 | let mut empty_pairs = Properties::new(); 88 | empty_pairs.insert_raw_for_key("bar", ""); 89 | empty_pairs.insert_raw_for_key("baz", ""); 90 | assert_eq!(empty_pairs.len(), 2); 91 | empty_pairs 92 | .apply_to(&mut props, "") 93 | .expect("Properties::apply_to should be infallible"); 94 | assert_eq!(props.len(), 3); 95 | assert_eq!(props.get_raw_for_key("bar"), Some(&crate::string::EMPTY)); 96 | } 97 | -------------------------------------------------------------------------------- /glob/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! # ec4rs-glob 2 | //! 3 | //! Refer to the README for an overview of this crate. 4 | //! 5 | //! ## Usage 6 | //! 7 | //! Create a [`Glob`] using [`Glob::new`], 8 | //! then match it against paths with [`Glob::matches`]. 9 | 10 | mod flatset; 11 | mod matcher; 12 | mod parser; 13 | mod splitter; 14 | mod stack; 15 | 16 | #[cfg(test)] 17 | mod tests; 18 | 19 | use flatset::FlatSet; 20 | use matcher::Matcher; 21 | use splitter::Splitter; 22 | 23 | #[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug)] 24 | /// A single glob pattern. 25 | pub struct Glob(Vec); 26 | 27 | impl Default for Glob { 28 | fn default() -> Self { 29 | Glob::empty() 30 | } 31 | } 32 | 33 | impl Glob { 34 | /// Returns an empty `Glob`. 35 | pub const fn empty() -> Glob { 36 | Self(Vec::new()) 37 | } 38 | 39 | /// Parses the provided pattern. 40 | /// 41 | /// This crate attempts to be maximally permissive in terms of accepted input and will treat 42 | /// common syntax errors, such as unclosed brackets, as if they were escaped. 43 | pub fn new(pattern: &str) -> Glob { 44 | parser::parse(pattern) 45 | } 46 | 47 | /// Returns `true` if the provided path matches this pattern. 48 | #[must_use] 49 | pub fn matches(&self, path: impl AsRef) -> bool { 50 | matcher::matches(path.as_ref(), self).is_some() 51 | } 52 | 53 | /// Append one [`Matcher`] to `self`. 54 | fn push(&mut self, matcher: Matcher) { 55 | // Optimizations, fusing certain kinds of matchers together. 56 | let push = !match &matcher { 57 | Matcher::Sep => { 58 | matches!(&self.0.last(), Some(Matcher::Sep)) 59 | } 60 | Matcher::Suffix(suffix) => { 61 | if let Some(Matcher::Suffix(prefix)) = self.0.last_mut() { 62 | prefix.push_str(suffix); 63 | true 64 | } else { 65 | false 66 | } 67 | } 68 | Matcher::AnySeq(true) => { 69 | matches!(&self.0.last(), Some(Matcher::AnySeq(false))) 70 | } 71 | _ => false, 72 | }; 73 | if push { 74 | self.0.push(matcher); 75 | } 76 | } 77 | 78 | /// Append one character as-is, with no special functionality.. 79 | fn append_escaped(&mut self, c: char) { 80 | if c == '/' { 81 | self.push(Matcher::Sep); 82 | } else if let Some(Matcher::Suffix(string)) = self.0.last_mut() { 83 | string.push(c); 84 | } else { 85 | // Since we know the Matcher::Suffix case in append() will always be false, 86 | // we can just save the optimizer the trouble. 87 | self.0.push(Matcher::Suffix(c.to_string())); 88 | } 89 | } 90 | 91 | /// Append all of the matchers from a pattern to `self`. 92 | fn append_glob(&mut self, glob: Glob) { 93 | self.0.reserve(glob.0.len()); 94 | for matcher in glob.0 { 95 | self.push(matcher) 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /glob/src/parser/alt.rs: -------------------------------------------------------------------------------- 1 | use crate::{Glob, Matcher}; 2 | 3 | pub struct AltStack(Vec); 4 | 5 | impl AltStack { 6 | pub const fn new() -> AltStack { 7 | AltStack(vec![]) 8 | } 9 | #[must_use] 10 | pub fn is_empty(&self) -> bool { 11 | self.0.is_empty() 12 | } 13 | 14 | pub fn push(&mut self, glob: Glob) { 15 | self.0.push(AltBuilder::new(glob)); 16 | } 17 | 18 | /// Adds a glob to the top builder of the stack. 19 | /// 20 | /// Returns the glob if there is no builder on the stack. 21 | #[must_use] 22 | pub fn add_alt(&mut self, glob: Glob) -> Option { 23 | if let Some(ab) = self.0.last_mut() { 24 | ab.add(glob); 25 | None 26 | } else { 27 | Some(glob) 28 | } 29 | } 30 | 31 | pub fn join_and_pop(&mut self, glob: Glob) -> (Glob, bool) { 32 | if let Some(mut builder) = self.0.pop() { 33 | builder.add(glob); 34 | (builder.join(), self.is_empty()) 35 | } else { 36 | (glob, true) 37 | } 38 | } 39 | 40 | pub fn add_alt_and_pop(&mut self, glob: Glob) -> (Glob, bool) { 41 | if let Some(mut builder) = self.0.pop() { 42 | builder.add(glob); 43 | (builder.build(), false) 44 | } else { 45 | (glob, true) 46 | } 47 | } 48 | } 49 | 50 | pub struct AltBuilder { 51 | glob: Glob, 52 | options: Vec, 53 | } 54 | 55 | impl AltBuilder { 56 | pub const fn new(glob: Glob) -> AltBuilder { 57 | AltBuilder { 58 | glob, 59 | options: vec![], 60 | } 61 | } 62 | pub fn add(&mut self, glob: Glob) { 63 | self.options.push(glob); 64 | } 65 | pub fn build(mut self) -> Glob { 66 | match self.options.len() { 67 | 0 => { 68 | self.glob.append_escaped('{'); 69 | self.glob.append_escaped('}'); 70 | self.glob 71 | } 72 | 1 => { 73 | self.glob.append_escaped('{'); 74 | for matcher in self.options.pop().unwrap().0 { 75 | self.glob.push(matcher); 76 | } 77 | self.glob.append_escaped('}'); 78 | self.glob 79 | } 80 | _ => { 81 | self.options 82 | .sort_by(|a, b| (!a.0.is_empty()).cmp(&!b.0.is_empty())); 83 | self.options.dedup(); 84 | self.glob.push(Matcher::Any(self.options.into())); 85 | self.glob 86 | } 87 | } 88 | } 89 | pub fn join(mut self) -> Glob { 90 | let mut first = true; 91 | self.glob.append_escaped('{'); 92 | for option in self.options { 93 | if first { 94 | first = false; 95 | } else { 96 | self.glob.append_escaped(',') 97 | } 98 | self.glob.append_glob(option); 99 | } 100 | self.glob 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/section.rs: -------------------------------------------------------------------------------- 1 | use crate::glob::Pattern; 2 | use crate::string::{ParseError, ToSharedString}; 3 | use crate::Properties; 4 | 5 | use std::path::Path; 6 | 7 | // Glob internals aren't stable enough to safely implement PartialEq here. 8 | 9 | /// One section of an EditorConfig file. 10 | #[derive(Clone)] 11 | pub struct Section { 12 | pattern: Result>, 13 | props: crate::Properties, 14 | } 15 | 16 | impl Section

{ 17 | /// Constructs a new [`Section`] that applies to files matching the specified pattern. 18 | /// 19 | /// If pattern parsing errors, the error will be retained internally 20 | /// and no paths will be considered to match the pattern. Errors can be detected with 21 | /// either [`or_err`][Self::or_err] or [`pattern`][Self::pattern]. 22 | pub fn new(pattern: &str) -> Self { 23 | Section { 24 | pattern: P::parse(pattern).map_err(|error| ParseError { 25 | error, 26 | string: pattern.into(), 27 | }), 28 | props: crate::Properties::new(), 29 | } 30 | } 31 | /// Returns `Ok(self)` if there was no pattern parse error, 32 | /// otherwise returns the error. 33 | pub fn or_err(self) -> Result> { 34 | if let Err(e) = self.pattern { 35 | Err(e) 36 | } else { 37 | Ok(self) 38 | } 39 | } 40 | /// Returns true if and only if this section applies to a file at the specified path. 41 | pub fn applies_to(&self, path: impl AsRef) -> bool { 42 | // MSRV of 1.56 prevents use of is_ok_and from 1.70. 43 | match self.pattern.as_ref() { 44 | Ok(p) => p.matches(path.as_ref()), 45 | _ => false, 46 | } 47 | } 48 | /// Returns a reference to either the pattern or the error. 49 | pub fn pattern(&self) -> &Result> { 50 | &self.pattern 51 | } 52 | /// Returns a shared reference to the internal [`Properties`] map. 53 | pub fn props(&self) -> &Properties { 54 | &self.props 55 | } 56 | /// Returns a mutable reference to the internal [`Properties`] map. 57 | pub fn props_mut(&mut self) -> &mut Properties { 58 | &mut self.props 59 | } 60 | /// Extracts the [`Properties`] map from `self`. 61 | pub fn into_props(self) -> Properties { 62 | self.props 63 | } 64 | /// Adds a property with the specified key, lowercasing the key. 65 | pub fn insert(&mut self, key: impl ToSharedString, val: impl ToSharedString) { 66 | self.props 67 | .insert_raw_for_key(key.to_shared_string().into_lowercase(), val) 68 | } 69 | } 70 | 71 | impl crate::PropertiesSource for &Section

{ 72 | /// Adds this section's properties to a [`Properties`]. 73 | /// 74 | /// This implementation is infallible. 75 | fn apply_to( 76 | self, 77 | props: &mut Properties, 78 | path: impl AsRef, 79 | ) -> Result<(), crate::Error> { 80 | let path_ref = path.as_ref(); 81 | if self.applies_to(path_ref) { 82 | let _ = self.props.apply_to(props, path_ref); 83 | } 84 | Ok(()) 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/tests/ecparser.rs: -------------------------------------------------------------------------------- 1 | use crate::glob::Glob; 2 | 3 | use crate::parser::ConfigParser; 4 | 5 | fn validate<'a>( 6 | text: &str, 7 | should_be_root: bool, 8 | expected: impl IntoIterator, 9 | ) { 10 | let path = std::path::Path::new(".editorconfig"); 11 | let mut parser = ConfigParser::<_, Glob>::new_buffered_with_path(text.as_bytes(), Some(path)) 12 | .expect("Should have created the parser"); 13 | assert_eq!(parser.is_root, should_be_root); 14 | for section_expected in expected { 15 | let section = parser.next().unwrap().unwrap(); 16 | let mut iter = section.props().iter(); 17 | #[allow(unused)] 18 | for (key, value, line_no) in section_expected { 19 | let (key_test, value_test) = iter.next().expect("Unexpected end of section"); 20 | assert_eq!(key_test, *key, "unexpected key"); 21 | assert_eq!(value_test.as_str(), *value, "unexpected value"); 22 | #[cfg(feature = "track-source")] 23 | assert_eq!( 24 | value_test.source().map(|(_, idx)| idx), 25 | Some(*line_no), 26 | "unexpected line number" 27 | ) 28 | } 29 | assert!(iter.next().is_none()); 30 | } 31 | assert!(parser.next().is_none()); 32 | } 33 | 34 | macro_rules! expect { 35 | [$([$(($key:literal, $value:literal, $line_no:literal)),*]),*] => { 36 | [$(&[$(($key, $value, $line_no)),*][..]),*] 37 | } 38 | } 39 | 40 | #[test] 41 | fn empty() { 42 | validate("", false, expect![]); 43 | } 44 | 45 | #[test] 46 | fn prelude() { 47 | validate("root = true\nroot = false", false, expect![]); 48 | validate("root = true", true, expect![]); 49 | validate("Root = True", true, expect![]); 50 | validate("# hello world", false, expect![]); 51 | } 52 | 53 | #[test] 54 | fn prelude_unknown() { 55 | validate("foo = bar", false, expect![]); 56 | validate("foo = bar\nroot = true", true, expect![]); 57 | } 58 | 59 | #[test] 60 | fn sections_empty() { 61 | validate("[foo]", false, expect![[]]); 62 | validate("[foo]\n[bar]", false, expect![[], []]); 63 | } 64 | 65 | #[test] 66 | fn sections() { 67 | validate( 68 | "[foo]\nbk=bv\nak=av", 69 | false, 70 | expect![[("bk", "bv", 2), ("ak", "av", 3)]], 71 | ); 72 | validate( 73 | "[foo]\nbk=bv\n[bar]\nak=av", 74 | false, 75 | expect![[("bk", "bv", 2)], [("ak", "av", 4)]], 76 | ); 77 | validate( 78 | "[foo]\nk=a\n[bar]\nk=b", 79 | false, 80 | expect![[("k", "a", 2)], [("k", "b", 4)]], 81 | ); 82 | } 83 | 84 | #[test] 85 | fn trailing_newline() { 86 | validate("[foo]\nbar=baz\n", false, expect![[("bar", "baz", 2)]]); 87 | validate("[foo]\nbar=baz\n\n", false, expect![[("bar", "baz", 2)]]); 88 | } 89 | 90 | #[test] 91 | fn section_with_comment_after_it() { 92 | validate( 93 | "[/*] # ignore this comment\nk=v", 94 | false, 95 | expect![[("k", "v", 2)]], 96 | ); 97 | } 98 | 99 | #[test] 100 | fn duplicate_key() { 101 | validate("[*]\nfoo=bar\nfoo=baz", false, expect![[("foo", "baz", 3)]]); 102 | } 103 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![doc = include_str!("../rustdoc.md")] 2 | #![deny(clippy::as_conversions)] 3 | #![deny(clippy::enum_glob_use)] 4 | #![deny(clippy::wildcard_imports)] 5 | #![deny(missing_docs)] 6 | #![deny(unsafe_code)] 7 | #![deny(rustdoc::bare_urls)] 8 | #![deny(rustdoc::broken_intra_doc_links)] 9 | #![deny(rustdoc::invalid_codeblock_attributes)] 10 | #![deny(rustdoc::invalid_html_tags)] 11 | #![deny(rustdoc::invalid_rust_codeblocks)] 12 | #![deny(rustdoc::private_intra_doc_links)] 13 | #![warn(clippy::if_then_some_else_none)] 14 | #![warn(clippy::pedantic)] 15 | #![allow(clippy::doc_markdown)] // reason = "False positives on EditorConfig". 16 | #![allow(clippy::module_name_repetitions)] // reason = "Affects re-exports from private modules." 17 | #![allow(clippy::must_use_candidate)] // reason = "Too pedantic." 18 | #![allow(clippy::semicolon_if_nothing_returned)] // reason = "Too pedantic." 19 | #![allow(clippy::let_underscore_untyped)] // reason = "Too pedantic." 20 | #![allow(clippy::needless_pass_by_value)] // reason = "FPs on Option" 21 | #![allow(clippy::missing_errors_doc)] // reason = "TODO: Fix." 22 | #![cfg_attr(doc_unstable, feature(doc_auto_cfg))] 23 | 24 | mod error; 25 | mod fallback; 26 | mod file; 27 | pub mod glob; 28 | mod linereader; 29 | mod parser; 30 | mod properties; 31 | pub mod property; 32 | mod section; 33 | pub mod string; 34 | #[cfg(test)] 35 | mod tests; 36 | mod traits; 37 | pub mod version; 38 | 39 | pub use error::{Error, ParseError}; 40 | pub use file::{ConfigFile, ConfigFiles}; 41 | pub use parser::ConfigParser; 42 | pub use properties::{Properties, PropertiesSource}; 43 | pub use section::Section; 44 | pub use traits::*; 45 | 46 | /// Retrieves the [`Properties`] for a file at the given path. 47 | /// 48 | /// This is the simplest way to use this library in an EditorConfig integration or plugin. 49 | /// 50 | /// This function does not canonicalize the path, 51 | /// but will join relative paths onto the current working directory. 52 | /// 53 | /// EditorConfig files are assumed to be named `.editorconfig`. 54 | /// If not, use [`properties_from_config_of`] 55 | pub fn properties_of( 56 | path: impl AsRef, 57 | ) -> Result { 58 | properties_from_config_of::

(path, Option::<&std::path::Path>::None) 59 | } 60 | 61 | /// Retrieves the [`Properties`] for a file at the given path, 62 | /// expecting EditorConfig files to be named matching `config_path_override`. 63 | /// 64 | /// This function does not canonicalize the path, 65 | /// but will join relative paths onto the current working directory. 66 | /// 67 | /// If the provided config path is absolute, uses the EditorConfig file at that path. 68 | /// If it's relative, joins it onto every ancestor of the target file, 69 | /// and looks for config files at those paths. 70 | /// If it's `None`, EditorConfig files are assumed to be named `.editorconfig`. 71 | pub fn properties_from_config_of( 72 | target_path: impl AsRef, 73 | config_path_override: Option>, 74 | ) -> Result { 75 | let mut retval = Properties::new(); 76 | ConfigFiles::

::open( 77 | target_path.as_ref(), 78 | config_path_override.as_ref().map(AsRef::as_ref), 79 | )? 80 | .apply_to(&mut retval, &target_path)?; 81 | Ok(retval) 82 | } 83 | -------------------------------------------------------------------------------- /glob/src/stack.rs: -------------------------------------------------------------------------------- 1 | use super::{Glob, Matcher, Splitter}; 2 | 3 | /// A stack for unwrapping globs to match them, 4 | /// as might happen with alternation. 5 | #[derive(Clone, Debug)] 6 | pub struct GlobStack<'a>(Vec<&'a [Matcher]>); 7 | 8 | impl<'a> GlobStack<'a> { 9 | pub fn new(starter: &Glob) -> GlobStack<'_> { 10 | GlobStack(vec![starter.0.as_slice()]) 11 | } 12 | 13 | pub fn add_glob(&mut self, glob: &'a Glob) { 14 | self.0.push(glob.0.as_slice()); 15 | } 16 | pub fn add_matcher(&mut self, matcher: &'a Matcher) { 17 | self.0.push(std::slice::from_ref(matcher)); 18 | } 19 | 20 | pub fn next(&mut self) -> Option<&'a Matcher> { 21 | // ^ impl Iterator? 22 | while let Some(front) = self.0.last_mut() { 23 | if let Some((retval, rest)) = front.split_last() { 24 | *front = rest; 25 | return Some(retval); 26 | } 27 | self.0.pop(); 28 | } 29 | None 30 | } 31 | } 32 | 33 | enum SavePoint<'a, 'b> { 34 | Rewind(Splitter<'a>, GlobStack<'b>, &'b Matcher), 35 | Alts(Splitter<'a>, GlobStack<'b>, &'b [Glob]), 36 | } 37 | 38 | /// A stack for saving and restoring state. 39 | pub struct SaveStack<'a, 'b> { 40 | globs: GlobStack<'b>, 41 | stack: Vec>, 42 | } 43 | 44 | impl<'a, 'b> SaveStack<'a, 'b> { 45 | pub fn new(_: &Splitter<'a>, glob: &'b Glob) -> SaveStack<'a, 'b> { 46 | SaveStack { 47 | globs: GlobStack::new(glob), 48 | stack: Vec::>::new(), 49 | } 50 | } 51 | pub fn globs(&mut self) -> &mut GlobStack<'b> { 52 | &mut self.globs 53 | } 54 | pub fn add_rewind(&mut self, splitter: Splitter<'a>, matcher: &'b Matcher) { 55 | self.stack 56 | .push(SavePoint::Rewind(splitter, self.globs.clone(), matcher)) 57 | } 58 | pub fn add_alts(&mut self, splitter: Splitter<'a>, matcher: &'b [Glob]) { 59 | if let Some((first, rest)) = matcher.split_first() { 60 | self.stack 61 | .push(SavePoint::Alts(splitter, self.globs.clone(), rest)); 62 | self.globs().add_glob(first); 63 | } 64 | } 65 | 66 | pub fn restore(&mut self) -> Option> { 67 | loop { 68 | // There's a continue in here, don't panic. 69 | break match self.stack.pop()? { 70 | SavePoint::Rewind(splitter, globs, matcher) => { 71 | self.stack.pop(); 72 | self.globs = globs; 73 | self.globs.add_matcher(matcher); 74 | Some(splitter) 75 | } 76 | SavePoint::Alts(splitter, globs, alts) => { 77 | self.globs = globs; 78 | if let Some((glob, rest)) = alts.split_first() { 79 | if !rest.is_empty() { 80 | self.stack.push(SavePoint::Alts( 81 | splitter.clone(), 82 | self.globs.clone(), 83 | rest, 84 | )); 85 | } 86 | self.globs.add_glob(glob); 87 | Some(splitter) 88 | } else { 89 | continue; 90 | } 91 | } 92 | }; 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /tools/src/bin/ec4rs-parse.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | use clap::Parser; 4 | use semver::{Version, VersionReq}; 5 | 6 | #[derive(Parser)] 7 | struct DisplayArgs { 8 | /// Prefix each line with the path to the file where the value originated 9 | #[clap(short = 'H', long)] 10 | with_filename: bool, 11 | /// Prefix each line with the line number where the value originated 12 | #[clap(short = 'n', long)] 13 | line_number: bool, 14 | /// Use the NUL byte as a field delimiter instead of ':' 15 | #[clap(short = '0', long)] 16 | null: bool, 17 | } 18 | 19 | #[derive(Parser)] 20 | #[clap(disable_version_flag = true)] 21 | struct Args { 22 | #[clap(flatten)] 23 | display: DisplayArgs, 24 | /// Override config filename 25 | #[clap(short)] 26 | filename: Option, 27 | /// Mostly ignored by this implementation 28 | #[clap(default_value = ec4rs::version::STRING, short = 'b')] 29 | ec_version: Version, 30 | /// Print test-friendly version information 31 | #[clap(short, long)] 32 | version: bool, 33 | files: Vec, 34 | } 35 | 36 | fn print_empty_prefix(display: &DisplayArgs) { 37 | if display.with_filename { 38 | print!("{}", if display.null { '\0' } else { ':' }); 39 | } 40 | if display.line_number { 41 | print!("{}", if display.null { '\0' } else { ':' }); 42 | } 43 | } 44 | 45 | fn print_config( 46 | path: &std::path::Path, 47 | filename: Option<&PathBuf>, 48 | legacy_fallbacks: bool, 49 | display: &DisplayArgs, 50 | ) { 51 | match ec4rs::properties_from_config_of::(path, filename) { 52 | Ok(mut props) => { 53 | if legacy_fallbacks { 54 | props.use_fallbacks_legacy(); 55 | } else { 56 | props.use_fallbacks(); 57 | } 58 | for (key, value) in props.iter() { 59 | let mut lc_value: Option = None; 60 | let value_ref = if ec4rs::property::STANDARD_KEYS.contains(&key) { 61 | lc_value.get_or_insert(value.into_lowercase()) 62 | } else { 63 | value 64 | }; 65 | if let Some((path, line_no)) = value_ref.source() { 66 | if display.with_filename { 67 | print!( 68 | "{}{}", 69 | path.to_string_lossy(), 70 | if display.null { '\0' } else { ':' } 71 | ); 72 | } 73 | if display.line_number { 74 | print!("{}{}", line_no, if display.null { '\0' } else { ':' }); 75 | } 76 | } else { 77 | print_empty_prefix(display); 78 | } 79 | println!("{}={}", key, value_ref) 80 | } 81 | } 82 | Err(e) => eprintln!("{}", e), 83 | } 84 | } 85 | 86 | fn main() { 87 | let args = Args::parse(); 88 | let legacy_ver = VersionReq::parse("<0.9.0").unwrap(); 89 | if args.version { 90 | println!( 91 | "EditorConfig (ec4rs-parse {}) Version {}", 92 | env!("CARGO_PKG_VERSION"), 93 | ec4rs::version::STRING 94 | ); 95 | } else if args.files.len() == 1 { 96 | print_config( 97 | args.files.first().unwrap(), 98 | args.filename.as_ref(), 99 | legacy_ver.matches(&args.ec_version), 100 | &args.display, 101 | ); 102 | } else { 103 | for path in args.files { 104 | print_empty_prefix(&args.display); 105 | println!("[{}]", path.to_string_lossy()); 106 | print_config( 107 | &path, 108 | args.filename.as_ref(), 109 | legacy_ver.matches(&args.ec_version), 110 | &args.display, 111 | ); 112 | } 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/linereader.rs: -------------------------------------------------------------------------------- 1 | use crate::ParseError; 2 | 3 | use std::io; 4 | 5 | #[derive(Clone, PartialEq, Eq, Debug)] 6 | pub enum Line<'a> { 7 | /// Either a comment or an empty line. 8 | Nothing, 9 | /// A section header, e.g. `[something.rs]` 10 | Section(&'a str), 11 | /// A propery/key-value pair, e.g. `indent_size = 2` 12 | Pair(&'a str, &'a str), 13 | } 14 | 15 | type LineReadResult<'a> = Result, ParseError>; 16 | 17 | /// Identifies the line type and extracts relevant slices. 18 | /// Does not do any lowercasing or anything beyond basic validation. 19 | /// 20 | /// It's usually not necessary to call this function directly. 21 | /// 22 | /// If the `allow-empty-values` feature is enabled, 23 | /// lines with a key but no value will be returned as a [`Line::Pair`]. 24 | /// Otherwise, they are considered invalid. 25 | pub fn parse_line(line: &str) -> LineReadResult<'_> { 26 | let mut l = line.trim_start(); 27 | if l.starts_with(is_comment) { 28 | return Ok(Line::Nothing); 29 | } 30 | 31 | // check for trailing comments after section headers 32 | let last_closing_bracket = l.rfind(']'); 33 | let last_comment = l.rfind(is_comment); 34 | 35 | if let (Some(bracket), Some(comment)) = (last_closing_bracket, last_comment) { 36 | if comment > bracket { 37 | // there is a comment following a closing bracket, trim it. 38 | l = l[0..comment].as_ref(); 39 | } 40 | } 41 | 42 | l = l.trim_end(); 43 | if l.is_empty() { 44 | Ok(Line::Nothing) 45 | } else if let Some(s) = l.strip_prefix('[').and_then(|s| s.strip_suffix(']')) { 46 | if s.is_empty() { 47 | Err(ParseError::InvalidLine) 48 | } else { 49 | Ok(Line::Section(s)) 50 | } 51 | } else if let Some((key_raw, val_raw)) = l.split_once('=') { 52 | let key = key_raw.trim_end(); 53 | let val = val_raw.trim_start(); 54 | match (key.is_empty(), val.is_empty()) { 55 | (true, _) => Err(ParseError::InvalidLine), 56 | (false, true) => Ok(Line::Pair(key.trim_end(), val)), 57 | (false, false) => Ok(Line::Pair(key.trim_end(), val.trim_start())), 58 | } 59 | } else { 60 | Err(ParseError::InvalidLine) 61 | } 62 | } 63 | 64 | /// Struct for extracting valid INI-like lines from text, 65 | /// suitable for initial parsing of individual .editorconfig files. 66 | /// Does minimal validation and does not modify the input text in any way. 67 | pub struct LineReader { 68 | ticker: usize, 69 | line: String, 70 | reader: R, 71 | } 72 | 73 | impl LineReader { 74 | /// Constructs a new line reader. 75 | pub fn new(r: R) -> LineReader { 76 | LineReader { 77 | ticker: 0, 78 | line: String::with_capacity(256), 79 | reader: r, 80 | } 81 | } 82 | 83 | /// Returns the line number of the contained line. 84 | pub fn line_no(&self) -> usize { 85 | self.ticker 86 | } 87 | 88 | /// Returns a reference to the contained line. 89 | pub fn line(&self) -> &str { 90 | self.line.as_str() 91 | } 92 | 93 | /// Parses the contained line using [`parse_line`]. 94 | /// 95 | /// It's usually not necessary to call this method. 96 | /// See [`LineReader::next`]. 97 | pub fn reparse(&self) -> LineReadResult<'_> { 98 | parse_line(self.line()) 99 | } 100 | 101 | /// Reads and parses the next line from the stream. 102 | pub fn next_line(&mut self) -> LineReadResult<'_> { 103 | self.line.clear(); 104 | match self.reader.read_line(&mut self.line) { 105 | Err(e) => Err(ParseError::Io(e)), 106 | Ok(0) => Err(ParseError::Eof), 107 | Ok(_) => { 108 | self.ticker += 1; 109 | if self.ticker == 1 { 110 | parse_line(self.line.strip_prefix('\u{FEFF}').unwrap_or(&self.line)) 111 | } else { 112 | self.reparse() 113 | } 114 | } 115 | } 116 | } 117 | } 118 | 119 | fn is_comment(c: char) -> bool { 120 | c == ';' || c == '#' 121 | } 122 | -------------------------------------------------------------------------------- /glob/src/tests.rs: -------------------------------------------------------------------------------- 1 | pub fn test<'a, 'b>( 2 | pattern: &str, 3 | valid: impl IntoIterator, 4 | invalid: impl IntoIterator, 5 | ) { 6 | use crate::Glob; 7 | let glob = Glob::new(pattern); 8 | for path in valid { 9 | assert!( 10 | glob.matches(path), 11 | "`{}` didn't match pattern `{}`; chain: {:?}", 12 | path, 13 | pattern, 14 | glob 15 | ) 16 | } 17 | for path in invalid { 18 | assert!( 19 | !glob.matches(path), 20 | "`{}` wrongly matched pattern `{}`; chain {:?}", 21 | path, 22 | pattern, 23 | glob 24 | ) 25 | } 26 | } 27 | 28 | #[test] 29 | fn basic() { 30 | test( 31 | "foo", 32 | ["foo", "/foo", "./foo", "/bar/foo"], 33 | ["/foobar", "/barfoo"], 34 | ); 35 | test("foo,bar", ["/foo,bar"], ["/foo", "/bar"]); 36 | } 37 | 38 | #[test] 39 | fn path() { 40 | test( 41 | "bar/foo", 42 | ["/bar/foo", "bar/foo", "/bar//foo"], 43 | ["/bar/foo/baz", "/baz/bar/foo"], 44 | ); 45 | } 46 | 47 | #[test] 48 | fn root_star() { 49 | test( 50 | "/*", 51 | ["/foo.txt", "/bar.xml", "/baz.json"], 52 | ["/bar/foo/baz.txt", "/baz/bar/foo.xml", "/bar/foo.txt"], 53 | ); 54 | } 55 | 56 | #[test] 57 | fn root_double_star() { 58 | test( 59 | "/**", 60 | [ 61 | "/foo.txt", 62 | "/bar.xml", 63 | "/baz.json", 64 | "/bar/foo/baz.txt", 65 | "/baz/bar/foo.xml", 66 | "/bar/foo.txt", 67 | ], 68 | [], 69 | ); 70 | } 71 | 72 | #[test] 73 | fn star() { 74 | test("*", ["/*", "/a"], []); 75 | test( 76 | "*.foo", 77 | ["/a.foo", "/b.foo", "/ab.foo", "/bar/abc.foo", "/.foo"], 78 | ["/foo"], 79 | ); 80 | test( 81 | "bar*.foo", 82 | ["/bar.foo", "/barab.foo", "/baz/bara.foo", "/bar.foo"], 83 | ["/bar/.foo"], 84 | ); 85 | } 86 | 87 | #[test] 88 | fn doublestar() { 89 | test("**.foo", ["/a.foo", "/a/a.foo", "/a/b.foo", "/.foo"], []); 90 | test( 91 | "a**d", 92 | ["/a/d", "/a/bd", "/a/bcd", "/a/b/c/d"], 93 | ["/bd", "/b/d", "/bcd"], 94 | ); 95 | } 96 | 97 | #[test] 98 | fn charclass_basic() { 99 | test("[a]", ["/a"], ["/aa", "/b"]); 100 | test("[a][b]", ["/ab"], ["/aa", "/ba", "/cab"]); 101 | test("[ab]", ["/a", "/b"], ["/ab"]); 102 | test("[!ab]", ["/c"], ["/a", "/b", "/ab", "/ac"]) 103 | } 104 | 105 | #[test] 106 | fn charclass_slash() { 107 | // See the brackets_slash_inside tests. 108 | test("a[b/]c", ["/a[b/]c"], ["/abc", "/a/c"]); 109 | } 110 | 111 | #[test] 112 | fn charclass_range() { 113 | test("[a-c]", ["/a", "/b", "/c"], ["/d"]); 114 | test("[-]", ["/-"], ["/"]); 115 | test("[-a]", ["/-", "/a"], []); 116 | test("[a-]", ["/-", "/a"], []); 117 | } 118 | 119 | #[test] 120 | fn charclass_escape() { 121 | test("[\\]a]", ["/]", "/a"], []); 122 | test("[a\\-c]", ["/a", "/-", "/c"], ["/b"]); 123 | test("[[-\\]^]", ["/[", "/]", "/^"], []); 124 | } 125 | 126 | #[test] 127 | fn numrange() { 128 | test("{1..3}", ["2"], ["1..3", "{1..3}"]); 129 | test("{8..11}", ["/8", "/9", "/10", "/11"], ["/12", "/1", "/01"]); 130 | test("{-3..-1}", ["/-3", "/-2", "/-1"], ["/0", "/1"]); 131 | test("{2..-1}", ["/2", "/1", "/0", "/-1"], ["/-2"]); 132 | } 133 | 134 | #[test] 135 | fn alt_basic() { 136 | test("{}", ["/{}"], ["/"]); 137 | test("{foo}", ["/{foo}"], ["/foo"]); 138 | test("{foo}.bar", ["/{foo}.bar"], ["/foo", "/foo.bar"]); 139 | test( 140 | "{foo,bar}", 141 | ["/foo", "/bar"], 142 | ["/foo,bar", "/foobar", "/{foo,bar}"], 143 | ); 144 | } 145 | 146 | #[test] 147 | fn alt_star() { 148 | test("{*}", ["/{}", "/{a}", "/{ab}"], []); 149 | test("{a,*}", ["/a", "/b"], []); 150 | } 151 | 152 | #[test] 153 | fn alt_unmatched() { 154 | test("{.foo", ["/{.foo"], ["/.foo", "/{.foo}"]); 155 | test("{},foo}", ["/{},foo}"], ["/.foo", "/.foo}"]); 156 | test("{,a,{b}", ["/{,a,{b}"], []); 157 | } 158 | 159 | #[test] 160 | fn alt_nested() { 161 | test("{a{bc,cd},e}", ["/abc", "/acd", "/e"], ["/cd"]); 162 | } 163 | 164 | #[test] 165 | fn alt_empty() { 166 | test("a{b,,c}", ["/a", "/ab", "/ac"], []); 167 | } 168 | -------------------------------------------------------------------------------- /glob/src/splitter.rs: -------------------------------------------------------------------------------- 1 | // Problem. 2 | // OsStr cannot be cast to &[u8] on Windows. 3 | // On Unixes and WASM it's fine. 4 | 5 | #[cfg(target_family = "unix")] 6 | mod cnv { 7 | use std::ffi::OsStr; 8 | #[allow(clippy::unnecessary_wraps)] 9 | pub fn to_bytes(s: &OsStr) -> Option<&[u8]> { 10 | use std::os::unix::ffi::OsStrExt; 11 | Some(s.as_bytes()) 12 | } 13 | } 14 | 15 | #[cfg(target_os = "wasi")] 16 | mod cnv { 17 | use std::ffi::OsStr; 18 | #[allow(clippy::unnecessary_wraps)] 19 | pub fn to_bytes(s: &OsStr) -> Option<&[u8]> { 20 | use std::os::wasi::ffi::OsStrExt; 21 | Some(s.as_bytes()) 22 | } 23 | } 24 | 25 | #[cfg(all(not(target_family = "unix"), not(target_os = "wasi")))] 26 | mod cnv { 27 | use std::ffi::OsStr; 28 | pub fn to_bytes(s: &OsStr) -> Option<&[u8]> { 29 | s.to_str().map(|s| s.as_ref()) 30 | } 31 | } 32 | 33 | #[derive(Clone)] 34 | pub struct Splitter<'a> { 35 | iter: std::path::Components<'a>, 36 | part: &'a [u8], 37 | matched_sep: bool, 38 | } 39 | 40 | impl<'a> Splitter<'a> { 41 | pub fn new(path: &'a std::path::Path) -> Option { 42 | Splitter { 43 | iter: path.components(), 44 | part: "".as_bytes(), 45 | matched_sep: false, 46 | } 47 | .next() 48 | } 49 | 50 | pub fn match_end(mut self) -> Option { 51 | use std::path::Component as C; 52 | if self.part.is_empty() { 53 | let next = self.iter.next_back(); 54 | if matches!(next, None | Some(C::CurDir | C::RootDir | C::Prefix(_))) { 55 | return Some(self); 56 | } 57 | } 58 | None 59 | } 60 | 61 | pub fn next(mut self) -> Option { 62 | use std::path::Component; 63 | self.part = match self.iter.next_back()? { 64 | Component::Normal(p) => cnv::to_bytes(p)?, 65 | Component::ParentDir => "..".as_bytes(), 66 | _ => "".as_bytes(), 67 | }; 68 | Some(self) 69 | } 70 | 71 | pub fn match_any(mut self, path_sep: bool) -> Option { 72 | if !self.part.is_empty() { 73 | self.part = self.part.split_last().unwrap().1; 74 | Some(self) 75 | } else if path_sep { 76 | self.match_sep()?.next() 77 | } else { 78 | None 79 | } 80 | } 81 | 82 | pub fn next_char(mut self) -> Option<(Self, char)> { 83 | if let Some((idx, c)) = self.find_next_char() { 84 | self.part = self.part.split_at(idx).0; 85 | Some((self, c)) 86 | } else { 87 | Some((self.next()?, '/')) 88 | } 89 | } 90 | 91 | fn find_next_char(&self) -> Option<(usize, char)> { 92 | let mut idx = self.part.len().checked_sub(1)?; 93 | let mut byte = self.part[idx]; 94 | while byte.leading_ones() == 1 { 95 | idx = idx.checked_sub(1)?; 96 | byte = self.part[idx]; 97 | } 98 | // TODO: Do the UTF-8 character decode here ourselves. 99 | let c = std::str::from_utf8(&self.part[idx..]) 100 | .ok()? 101 | .chars() 102 | .next_back()?; 103 | Some((idx, c)) 104 | } 105 | 106 | pub fn match_sep(mut self) -> Option { 107 | let is_empty = self.part.is_empty(); 108 | is_empty.then(|| { 109 | self.matched_sep = true; 110 | self 111 | }) 112 | } 113 | 114 | pub fn match_suffix(mut self, suffix: &str) -> Option { 115 | if self.part.is_empty() && self.matched_sep { 116 | self.matched_sep = false; 117 | self = self.next()?; 118 | } 119 | if let Some(rest) = self.part.strip_suffix(suffix.as_bytes()) { 120 | self.part = rest; 121 | Some(self) 122 | } else { 123 | None 124 | } 125 | } 126 | 127 | pub fn match_number(mut self, lower: isize, upper: isize) -> Option { 128 | let mut q = std::collections::VecDeque::::new(); 129 | let mut allow_zero: bool = true; 130 | let mut last_ok = self.clone(); 131 | while let Some((next_ok, c)) = self.next_char() { 132 | if c.is_numeric() && (c != '0' || allow_zero) { 133 | last_ok = next_ok.clone(); 134 | allow_zero = c == '0'; 135 | q.push_front(c); 136 | } else if c == '-' { 137 | last_ok = next_ok.clone(); 138 | q.push_front('-'); 139 | break; 140 | } else { 141 | break; 142 | } 143 | self = next_ok; 144 | } 145 | let i = q.iter().collect::().parse::().ok()?; 146 | if i < lower || i > upper { 147 | return None; 148 | } 149 | Some(last_ok) 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /src/file.rs: -------------------------------------------------------------------------------- 1 | use std::path::{Path, PathBuf}; 2 | 3 | use crate::{ 4 | glob::Pattern, ConfigParser, Error, ParseError, Properties, PropertiesSource, Section, 5 | }; 6 | 7 | /// Convenience wrapper for an [`ConfigParser`] that reads files. 8 | pub struct ConfigFile { 9 | // TODO: Arc. It's more important to have cheap clones than mutability. 10 | /// The path to the open file. 11 | pub path: PathBuf, 12 | /// A [`ConfigParser`] that reads from the file. 13 | pub reader: ConfigParser, P>, 14 | } 15 | 16 | impl ConfigFile

{ 17 | /// Opens a file for reading and uses it to construct an [`ConfigParser`]. 18 | /// 19 | /// If the file cannot be opened, wraps the [`std::io::Error`] in a [`ParseError`]. 20 | pub fn open(path: impl AsRef) -> Result, ParseError> { 21 | let file = std::fs::File::open(&path).map_err(ParseError::Io)?; 22 | let reader = ConfigParser::new_buffered_with_path(file, Some(path.as_ref()))?; 23 | Ok(ConfigFile { 24 | path: path.as_ref().to_owned(), 25 | reader, 26 | }) 27 | } 28 | 29 | /// Wraps a [`ParseError`] in an [`Error::InFile`]. 30 | /// 31 | /// Uses the path and current line number from this instance. 32 | pub fn add_error_context(&self, error: ParseError) -> Error { 33 | Error::InFile(self.path.clone(), self.reader.line_no(), error) 34 | } 35 | } 36 | 37 | impl Iterator for ConfigFile

{ 38 | type Item = Result, ParseError>; 39 | fn next(&mut self) -> Option { 40 | self.reader.next() 41 | } 42 | } 43 | 44 | impl std::iter::FusedIterator for ConfigFile

{} 45 | 46 | impl PropertiesSource for &mut ConfigFile

{ 47 | /// Adds properties from the file's sections to the specified [`Properties`] map. 48 | /// 49 | /// Uses [`ConfigFile::path`] when determining applicability to stop `**` from going too far. 50 | /// Returns parse errors wrapped in an [`Error::InFile`]. 51 | fn apply_to(self, props: &mut Properties, path: impl AsRef) -> Result<(), crate::Error> { 52 | let get_parent = || self.path.parent(); 53 | let path = if let Some(parent) = get_parent() { 54 | let path = path.as_ref(); 55 | path.strip_prefix(parent).unwrap_or(path) 56 | } else { 57 | path.as_ref() 58 | }; 59 | match self.reader.apply_to(props, path) { 60 | Ok(()) => Ok(()), 61 | Err(crate::Error::Parse(e)) => Err(self.add_error_context(e)), 62 | Err(e) => panic!("unexpected error variant {:?}", e), 63 | } 64 | } 65 | } 66 | 67 | /// Directory traverser for finding and opening EditorConfig files. 68 | /// 69 | /// All the contained files are open for reading and have not had any sections read. 70 | /// When iterated over, either by using it as an [`Iterator`] 71 | /// or by calling [`ConfigFiles::iter`], 72 | /// returns [`ConfigFile`]s in the order that they would apply to a [`Properties`] map. 73 | pub struct ConfigFiles(Vec>); 74 | 75 | impl ConfigFiles

{ 76 | /// Searches for EditorConfig files that might apply to a file at the specified path. 77 | /// 78 | /// This function does not canonicalize the path, 79 | /// but will join relative paths onto the current working directory. 80 | /// 81 | /// EditorConfig files are assumed to be named `.editorconfig` 82 | /// unless an override is supplied as the second argument. 83 | #[allow(clippy::needless_pass_by_value)] 84 | pub fn open( 85 | path: impl AsRef, 86 | config_name: Option>, 87 | ) -> Result { 88 | use std::borrow::Cow; 89 | let filename = config_name 90 | .as_ref() 91 | .map_or_else(|| ".editorconfig".as_ref(), |f| f.as_ref()); 92 | Ok(ConfigFiles(if filename.is_relative() { 93 | let mut abs_path = Cow::from(path.as_ref()); 94 | if abs_path.is_relative() { 95 | abs_path = std::env::current_dir() 96 | .map_err(Error::InvalidCwd)? 97 | .join(&path) 98 | .into() 99 | } 100 | let mut path = abs_path.as_ref(); 101 | let mut vec = Vec::new(); 102 | while let Some(dir) = path.parent() { 103 | if let Ok(file) = ConfigFile::open(dir.join(filename)) { 104 | let should_break = file.reader.is_root; 105 | vec.push(file); 106 | if should_break { 107 | break; 108 | } 109 | } 110 | path = dir; 111 | } 112 | vec 113 | } else { 114 | // TODO: Better errors. 115 | vec![ConfigFile::open(filename).map_err(Error::Parse)?] 116 | })) 117 | } 118 | 119 | /// Returns an iterator over the contained [`ConfigFiles`]. 120 | pub fn iter(&self) -> impl Iterator> { 121 | self.0.iter().rev() 122 | } 123 | 124 | // To maintain the invariant that these files have not had any sections read, 125 | // there is no `iter_mut` method. 126 | } 127 | 128 | impl Iterator for ConfigFiles

{ 129 | type Item = ConfigFile

; 130 | fn next(&mut self) -> Option> { 131 | self.0.pop() 132 | } 133 | } 134 | 135 | impl std::iter::FusedIterator for ConfigFiles

{} 136 | 137 | impl PropertiesSource for ConfigFiles

{ 138 | /// Adds properties from the files' sections to the specified [`Properties`] map. 139 | /// 140 | /// Ignores the files' paths when determining applicability. 141 | fn apply_to(self, props: &mut Properties, path: impl AsRef) -> Result<(), crate::Error> { 142 | let path = path.as_ref(); 143 | for mut file in self { 144 | file.apply_to(props, path)?; 145 | } 146 | Ok(()) 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /src/parser.rs: -------------------------------------------------------------------------------- 1 | use crate::glob::Pattern; 2 | use crate::linereader::LineReader; 3 | use crate::ParseError; 4 | use crate::Section; 5 | use std::io; 6 | use std::path::Path; 7 | 8 | /// Parser for the text of an EditorConfig file. 9 | /// 10 | /// This struct wraps any [`BufRead`][std::io::BufRead]. 11 | /// It eagerly parses the preamble on construction. 12 | /// [`Section`]s may then be parsed by calling [`ConfigParser::read_section`]. 13 | pub struct ConfigParser { 14 | /// Incidates if a `root = true` line was found in the preamble. 15 | pub is_root: bool, 16 | eof: bool, 17 | reader: LineReader, 18 | #[allow(clippy::type_complexity)] 19 | glob_marker: std::marker::PhantomData Result>, 20 | #[cfg(feature = "track-source")] 21 | path: Option>, 22 | } 23 | 24 | impl ConfigParser, P> { 25 | /// Convenience function for construction using an unbuffered [`io::Read`]. 26 | /// 27 | /// See [`ConfigParser::new`]. 28 | pub fn new_buffered(source: R) -> Result, P>, ParseError> { 29 | Self::new(io::BufReader::new(source)) 30 | } 31 | /// Convenience function for construction using an unbuffered [`io::Read`] 32 | /// which is assumed to be a file at `path`. 33 | /// 34 | /// See [`ConfigParser::new_with_path`]. 35 | pub fn new_buffered_with_path( 36 | source: R, 37 | path: Option>, 38 | ) -> Result, P>, ParseError> { 39 | Self::new_with_path(io::BufReader::new(source), path.as_ref()) 40 | } 41 | } 42 | 43 | impl ConfigParser { 44 | /// Constructs a new [`ConfigParser`] and reads the preamble from the provided source, 45 | /// which is assumed to be a file at `path`. 46 | /// 47 | /// Returns `Ok` if the preamble was parsed successfully, 48 | /// otherwise returns `Err` with the error that occurred during reading. 49 | pub fn new_with_path( 50 | buf_source: R, 51 | #[allow(unused)] path: Option>, 52 | ) -> Result { 53 | let mut reader = LineReader::new(buf_source); 54 | let mut is_root = false; 55 | let eof = loop { 56 | use crate::linereader::Line; 57 | match reader.next_line() { 58 | Err(ParseError::Eof) => break true, 59 | Err(e) => return Err(e), 60 | Ok(Line::Nothing) => (), 61 | Ok(Line::Section(_)) => break false, 62 | Ok(Line::Pair(k, v)) => { 63 | if "root".eq_ignore_ascii_case(k) { 64 | if let Ok(b) = v.to_ascii_lowercase().parse::() { 65 | is_root = b; 66 | } 67 | } 68 | // Quietly ignore unknown properties. 69 | } 70 | } 71 | }; 72 | Ok(ConfigParser { 73 | is_root, 74 | eof, 75 | reader, 76 | glob_marker: std::marker::PhantomData, 77 | #[cfg(feature = "track-source")] 78 | path: path.map(|p| crate::string::Shared::from(p.as_ref())), 79 | }) 80 | } 81 | /// Constructs a new [`ConfigParser`] and reads the preamble from the provided source. 82 | /// 83 | /// Returns `Ok` if the preamble was parsed successfully, 84 | /// otherwise returns `Err` with the error that occurred during reading. 85 | pub fn new(buf_source: R) -> Result { 86 | Self::new_with_path(buf_source, Option::<&Path>::None) 87 | } 88 | 89 | /// Returns `true` if there may be another section to read. 90 | pub fn has_more(&self) -> bool { 91 | self.eof 92 | } 93 | 94 | /// Returns the current line number. 95 | pub fn line_no(&self) -> usize { 96 | self.reader.line_no() 97 | } 98 | 99 | /// Parses a [`Section`], reading more if needed. 100 | pub fn read_section(&mut self) -> Result, ParseError> { 101 | use crate::linereader::Line; 102 | if self.eof { 103 | return Err(ParseError::Eof); 104 | } 105 | if let Ok(Line::Section(header)) = self.reader.reparse() { 106 | let mut section = Section::new(header); 107 | loop { 108 | // Get line_no here to avoid borrowing issues, increment for 1-based indices. 109 | #[cfg(feature = "track-source")] 110 | let line_no = self.reader.line_no() + 1; 111 | match self.reader.next_line() { 112 | Err(e) => { 113 | self.eof = true; 114 | break if matches!(e, ParseError::Eof) { 115 | Ok(section) 116 | } else { 117 | Err(e) 118 | }; 119 | } 120 | Ok(Line::Section(_)) => break Ok(section), 121 | Ok(Line::Nothing) => (), 122 | Ok(Line::Pair(k, v)) => { 123 | #[allow(unused_mut)] 124 | let mut v = crate::string::SharedString::new(v); 125 | #[cfg(feature = "track-source")] 126 | if let Some(path) = self.path.as_ref() { 127 | v.set_source(path.clone(), line_no); 128 | } 129 | section.insert(k, v); 130 | } 131 | } 132 | } 133 | } else { 134 | Err(ParseError::InvalidLine) 135 | } 136 | } 137 | } 138 | 139 | impl Iterator for ConfigParser { 140 | type Item = Result, ParseError>; 141 | fn next(&mut self) -> Option { 142 | match self.read_section() { 143 | Ok(r) => Some(Ok(r)), 144 | Err(ParseError::Eof) => None, 145 | Err(e) => Some(Err(e)), 146 | } 147 | } 148 | } 149 | 150 | impl std::iter::FusedIterator for ConfigParser {} 151 | 152 | impl crate::PropertiesSource for &mut ConfigParser { 153 | fn apply_to( 154 | self, 155 | props: &mut crate::Properties, 156 | path: impl AsRef, 157 | ) -> Result<(), crate::Error> { 158 | let path = path.as_ref(); 159 | for section_result in self { 160 | match section_result { 161 | Ok(section) => { 162 | let _ = section.apply_to(props, path); 163 | } 164 | Err(error) => return Err(crate::Error::Parse(error)), 165 | } 166 | } 167 | Ok(()) 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /src/property.rs: -------------------------------------------------------------------------------- 1 | //! Enums for common EditorConfig properties. 2 | //! 3 | //! This crate contains every current universal property specified by standard, 4 | //! plus others that are common enough to be worth supporting. 5 | //! All of them are non-exhaustive enums in order to support future additions to the standard 6 | //! as well as handle the common special value `"unset"`. 7 | 8 | mod language_tag; 9 | 10 | pub use language_tag::*; 11 | 12 | use super::{PropertyKey, PropertyValue}; 13 | use crate::string::SharedString; 14 | 15 | use std::fmt::Display; 16 | 17 | /// Error for common property parse failures. 18 | #[derive(Clone, Copy, Debug)] 19 | pub struct UnknownValueError; 20 | 21 | impl Display for UnknownValueError { 22 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 23 | write!(f, "unknown value") 24 | } 25 | } 26 | 27 | impl std::error::Error for UnknownValueError {} 28 | 29 | // TODO: Deduplicate these macros a bit? 30 | 31 | macro_rules! property_choice { 32 | ($prop_id:ident, $name:literal; $(($variant:ident, $string:literal)),+) => { 33 | #[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Default)] 34 | #[doc = concat!("The [`",$name,"`](https://github.com/editorconfig/editorconfig/wiki/EditorConfig-Properties#",$name,") property.")] 35 | #[allow(missing_docs)] 36 | #[non_exhaustive] 37 | pub enum $prop_id { 38 | #[default] 39 | Unset, 40 | $($variant),+ 41 | } 42 | 43 | impl std::str::FromStr for $prop_id { 44 | type Err = UnknownValueError; 45 | fn from_str(raw: &str) -> Result { 46 | match &*crate::string::into_lowercase(raw) { 47 | $($string => Ok($prop_id::$variant),)+ 48 | _ => Err(UnknownValueError) 49 | } 50 | } 51 | } 52 | 53 | impl crate::string::ToSharedString for $prop_id { 54 | fn to_shared_string(self) -> SharedString { 55 | SharedString::new_static(match self { 56 | $prop_id::Unset => "unset", 57 | $($prop_id::$variant => $string),* 58 | }) 59 | } 60 | 61 | fn try_as_str(&self) -> Option<&str> { 62 | Some(match self { 63 | $prop_id::Unset => "unset", 64 | $($prop_id::$variant => $string),* 65 | }) 66 | } 67 | } 68 | 69 | impl PropertyValue for $prop_id {} 70 | 71 | impl PropertyKey for $prop_id { 72 | fn key() -> &'static str {$name} 73 | } 74 | 75 | impl Display for $prop_id { 76 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 77 | match self { 78 | $prop_id::Unset => "unset".fmt(f), 79 | $($prop_id::$variant => $string.fmt(f)),* 80 | } 81 | } 82 | } 83 | } 84 | } 85 | 86 | macro_rules! property_valued { 87 | ( 88 | $prop_id:ident, $name:literal, $value_type:ty; 89 | $(($variant:ident, $string:literal)),* 90 | ) => { 91 | #[derive(Clone, Copy, PartialEq, Eq, Debug, Default)] 92 | #[doc = concat!("The [`",$name,"`](https://github.com/editorconfig/editorconfig/wiki/EditorConfig-Properties#",$name,") property.")] 93 | #[allow(missing_docs)] 94 | #[non_exhaustive] 95 | pub enum $prop_id { 96 | #[default] 97 | Unset, 98 | Value($value_type) 99 | $(,$variant)* 100 | } 101 | 102 | impl std::str::FromStr for $prop_id { 103 | type Err = UnknownValueError; 104 | fn from_str(raw: &str) -> Result { 105 | match &*crate::string::into_lowercase(raw) { 106 | "unset" => Ok($prop_id::Unset), 107 | $($string => Ok($prop_id::$variant),)* 108 | v => v.parse::<$value_type>().map(Self::Value).or(Err(UnknownValueError)) 109 | } 110 | } 111 | } 112 | 113 | impl crate::string::ToSharedString for $prop_id { 114 | fn to_shared_string(self) -> SharedString { 115 | match self { 116 | $prop_id::Unset => SharedString::new_static("unset"), 117 | $prop_id::Value(v) => SharedString::new(v.to_string()), 118 | $($prop_id::$variant => SharedString::new_static($string)),* 119 | } 120 | } 121 | 122 | fn try_as_str(&self) -> Option<&str> { 123 | match self { 124 | $prop_id::Unset => Some("unset"), 125 | $prop_id::Value(_) => None, 126 | $($prop_id::$variant => Some($string)),* 127 | } 128 | } 129 | } 130 | 131 | impl PropertyValue for $prop_id {} 132 | 133 | impl PropertyKey for $prop_id { 134 | fn key() -> &'static str {$name} 135 | } 136 | 137 | impl Display for $prop_id { 138 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 139 | match self { 140 | $prop_id::Unset => "unset".fmt(f), 141 | $prop_id::Value(v) => v.fmt(f), 142 | $($prop_id::$variant => $string.fmt(f)),* 143 | } 144 | } 145 | } 146 | } 147 | } 148 | 149 | property_choice! { 150 | IndentStyle, "indent_style"; 151 | (Tabs, "tab"), 152 | (Spaces, "space") 153 | } 154 | 155 | // NOTE: 156 | // The spec and the wiki disagree on the valid range of indent/tab sizes. 157 | // The spec says "whole numbers" for both, 158 | // whereas the wiki says "an integer"/"a positive integer" respectively. 159 | // This implementation follows the spec strictly here. 160 | // Notably, it will happily consider sizes of 0 valid. 161 | 162 | property_valued! {IndentSize, "indent_size", usize; (UseTabWidth, "tab")} 163 | property_valued! {TabWidth, "tab_width", usize;} 164 | 165 | property_choice! { 166 | EndOfLine, "end_of_line"; 167 | (Lf, "lf"), 168 | (CrLf, "crlf"), 169 | (Cr, "cr") 170 | } 171 | 172 | property_choice! { 173 | Charset, "charset"; 174 | (Utf8, "utf-8"), 175 | (Latin1, "latin1"), 176 | (Utf16Le, "utf-16le"), 177 | (Utf16Be, "utf-16be"), 178 | (Utf8Bom, "utf-8-bom") 179 | } 180 | 181 | property_valued! {TrimTrailingWs, "trim_trailing_whitespace", bool;} 182 | property_valued! {FinalNewline, "insert_final_newline", bool;} 183 | property_valued! {MaxLineLen, "max_line_length", usize;} 184 | 185 | /// The `spelling_language` property added by EditorConfig 0.16. 186 | /// 187 | /// This type's [`PropertyValue`] implementation, by default, 188 | /// adheres strictly to the EditorConfig spec. 189 | #[derive(Clone, PartialEq, Eq, Hash, Debug, Default)] 190 | #[allow(missing_docs)] 191 | #[non_exhaustive] 192 | pub enum SpellingLanguage { 193 | #[default] 194 | Unset, 195 | Value(LanguageTag), 196 | } 197 | 198 | impl std::str::FromStr for SpellingLanguage { 199 | type Err = UnknownValueError; 200 | fn from_str(raw: &str) -> Result { 201 | if raw.eq_ignore_ascii_case("unset") { 202 | Ok(SpellingLanguage::Unset) 203 | } else { 204 | LanguageTag::try_from(SharedString::new(raw)).map(SpellingLanguage::Value) 205 | } 206 | } 207 | } 208 | 209 | impl crate::string::ToSharedString for SpellingLanguage { 210 | fn to_shared_string(self) -> SharedString { 211 | match self { 212 | SpellingLanguage::Unset => crate::string::UNSET.clone(), 213 | SpellingLanguage::Value(retval) => SharedString::new(retval.to_string()), 214 | } 215 | } 216 | 217 | fn try_as_str(&self) -> Option<&str> { 218 | match self { 219 | SpellingLanguage::Unset => Some("unset"), 220 | SpellingLanguage::Value(_) => None, 221 | } 222 | } 223 | } 224 | 225 | impl PropertyValue for SpellingLanguage { 226 | fn from_shared_string(raw: &SharedString) -> Result { 227 | if raw.eq_ignore_ascii_case("unset") { 228 | Ok(SpellingLanguage::Unset) 229 | } else { 230 | LanguageTag::try_from(raw.clone()).map(SpellingLanguage::Value) 231 | } 232 | } 233 | } 234 | 235 | impl PropertyKey for SpellingLanguage { 236 | fn key() -> &'static str { 237 | "spelling_language" 238 | } 239 | } 240 | 241 | impl Display for SpellingLanguage { 242 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 243 | match self { 244 | SpellingLanguage::Unset => crate::string::UNSET.fmt(f), 245 | SpellingLanguage::Value(v) => v.fmt(f), 246 | } 247 | } 248 | } 249 | 250 | /// All the keys of the standard properties. 251 | /// 252 | /// Can be used to determine if a property is defined in the specification or not. 253 | pub static STANDARD_KEYS: &[&str] = &[ 254 | "indent_size", 255 | "indent_style", 256 | "tab_width", 257 | "end_of_line", 258 | "charset", 259 | "trim_trailing_whitespace", 260 | "insert_final_newline", 261 | "spelling_language", 262 | // NOT "max_line_length". 263 | ]; 264 | -------------------------------------------------------------------------------- /src/string.rs: -------------------------------------------------------------------------------- 1 | //! The `SharedString` type and supporting types. 2 | 3 | use std::borrow::Cow; 4 | 5 | // Shared is a purely internal type alias. 6 | // Its usage requires it to implement From and Deref. 7 | 8 | pub(crate) type Shared = std::sync::Arc; 9 | 10 | #[cfg(feature = "track-source")] 11 | mod source { 12 | use std::path::Path; 13 | 14 | #[derive(Clone)] 15 | pub struct Source { 16 | path: super::Shared, 17 | line: usize, 18 | } 19 | 20 | impl std::fmt::Debug for Source { 21 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 22 | write!(f, "{}:{}", self.path.to_string_lossy(), self.line) 23 | } 24 | } 25 | 26 | impl Source { 27 | pub fn new(path: &std::path::Path, line: usize) -> Self { 28 | Source { 29 | path: super::Shared::from(path), 30 | line, 31 | } 32 | } 33 | pub fn get(&self) -> (&std::path::Path, usize) { 34 | (super::Shared::as_ref(&self.path), self.line) 35 | } 36 | } 37 | } 38 | 39 | // TODO: Eventually add support for an Arc-like type that uses a thin pointer here. 40 | // Probably not triomphe::ThinArc, since we'd need to use unsafe to use it. 41 | 42 | #[derive(Clone, Debug)] 43 | enum SharedStringInner { 44 | Static(&'static str), 45 | Owned(Shared), 46 | } 47 | 48 | impl SharedStringInner { 49 | #[inline] 50 | pub fn new(string: &str) -> Self { 51 | SharedStringInner::Owned(Shared::from(string)) 52 | } 53 | #[inline] 54 | pub fn get(&self) -> &str { 55 | match self { 56 | SharedStringInner::Static(v) => v, 57 | SharedStringInner::Owned(v) => Shared::as_ref(v), 58 | } 59 | } 60 | } 61 | 62 | impl Default for SharedStringInner { 63 | fn default() -> Self { 64 | SharedStringInner::Static("") 65 | } 66 | } 67 | 68 | impl std::ops::Deref for SharedStringInner { 69 | type Target = str; 70 | 71 | fn deref(&self) -> &Self::Target { 72 | self.get() 73 | } 74 | } 75 | 76 | /// A shared immutable string type. 77 | /// 78 | /// Internally, this is either a `&'static str` or an atomically ref-counted `str`. 79 | /// It's meant to represent either keys or values with minimal allocations 80 | /// or duplication of data in memory, in exchange for not supporting mutation 81 | /// or even in-place slicing. 82 | /// 83 | /// With the `track-source` feature, 84 | /// objects of this type can also track the file and line number they originate from. 85 | #[derive(Clone, Debug)] 86 | pub struct SharedString { 87 | value: SharedStringInner, 88 | #[cfg(feature = "track-source")] 89 | source: Option, 90 | } 91 | 92 | /// A `SharedString` equal to `"unset"`. 93 | pub static UNSET: SharedString = SharedString { 94 | value: SharedStringInner::Static("unset"), 95 | #[cfg(feature = "track-source")] 96 | source: None, 97 | }; 98 | 99 | /// A `SharedString` equal to `""` (an empty string). 100 | pub static EMPTY: SharedString = SharedString { 101 | value: SharedStringInner::Static(""), 102 | #[cfg(feature = "track-source")] 103 | source: None, 104 | }; 105 | 106 | // Manual-impl (Partial)Eq, (Partial)Ord, and Hash so that the source isn't considered. 107 | 108 | impl PartialEq for SharedString { 109 | fn eq(&self, other: &Self) -> bool { 110 | *self.value == *other.value 111 | } 112 | } 113 | impl Eq for SharedString {} 114 | impl PartialOrd for SharedString { 115 | fn partial_cmp(&self, other: &Self) -> Option { 116 | Some(self.value.cmp(&other.value)) 117 | } 118 | } 119 | impl Ord for SharedString { 120 | fn cmp(&self, other: &Self) -> std::cmp::Ordering { 121 | self.value.cmp(&other.value) 122 | } 123 | } 124 | impl std::hash::Hash for SharedString { 125 | fn hash(&self, state: &mut H) { 126 | state.write(self.value.as_bytes()); 127 | state.write_u8(0); 128 | } 129 | } 130 | 131 | impl std::borrow::Borrow for SharedString { 132 | fn borrow(&self) -> &str { 133 | &self.value 134 | } 135 | } 136 | 137 | impl AsRef for SharedString { 138 | fn as_ref(&self) -> &str { 139 | &self.value 140 | } 141 | } 142 | impl AsRef<[u8]> for SharedString { 143 | fn as_ref(&self) -> &[u8] { 144 | self.value.as_bytes() 145 | } 146 | } 147 | 148 | impl std::ops::Deref for SharedString { 149 | type Target = str; 150 | 151 | fn deref(&self) -> &Self::Target { 152 | &self.value 153 | } 154 | } 155 | 156 | impl Default for SharedString { 157 | fn default() -> Self { 158 | EMPTY.clone() 159 | } 160 | } 161 | 162 | impl Default for &SharedString { 163 | fn default() -> Self { 164 | &EMPTY 165 | } 166 | } 167 | 168 | impl SharedString { 169 | /// Creates `Self` from the provided string. 170 | /// 171 | /// This function copies the string. If the string is `'static`, consider 172 | /// using [`SharedString::new_static`] instead. 173 | pub fn new(value: impl AsRef) -> Self { 174 | SharedString { 175 | value: SharedStringInner::new(value.as_ref()), 176 | #[cfg(feature = "track-source")] 177 | source: None, 178 | } 179 | } 180 | /// As [`SharedString::new`] but only accepts a `&'static str`. 181 | /// 182 | /// This function does not copy the string. 183 | pub fn new_static(value: &'static str) -> Self { 184 | SharedString { 185 | value: SharedStringInner::Static(value), 186 | #[cfg(feature = "track-source")] 187 | source: None, 188 | } 189 | } 190 | /// Extracts a string slice containing the entire `SharedString`. 191 | pub fn as_str(&self) -> &str { 192 | self 193 | } 194 | #[cfg(feature = "track-source")] 195 | /// Returns the path to the file and the line number that this value originates from. 196 | /// 197 | /// The line number is 1-indexed to match convention; 198 | /// the first line will have a line number of 1 rather than 0. 199 | pub fn source(&self) -> Option<(&std::path::Path, usize)> { 200 | self.source.as_ref().map(source::Source::get) 201 | } 202 | 203 | #[cfg(feature = "track-source")] 204 | /// Sets the path and line number from which this value originated. 205 | /// 206 | /// The line number should be 1-indexed to match convention; 207 | /// the first line should have a line number of 1 rather than 0. 208 | pub fn set_source(&mut self, path: impl AsRef, line: usize) { 209 | self.source = Some(source::Source::new(path.as_ref(), line)) 210 | } 211 | 212 | #[cfg(feature = "track-source")] 213 | /// Efficiently clones the source from `other`. 214 | pub fn set_source_from(&mut self, other: &SharedString) { 215 | self.source.clone_from(&other.source); 216 | } 217 | 218 | /// Clears the path and line number from which this value originated. 219 | /// 220 | /// If the `track-source` feature is not enabled, this function is a no-op. 221 | pub fn clear_source(&mut self) { 222 | #[cfg(feature = "track-source")] 223 | { 224 | self.source = None; 225 | } 226 | } 227 | 228 | /// Returns a lowercased version of `self`. Will not allocate if `self` is already lowercase. 229 | #[must_use] 230 | pub fn into_lowercase(&self) -> Self { 231 | // TODO: This requires two iterations over the string, and it's definitely possible 232 | // to do it in one. 233 | match into_lowercase(self.value.as_ref()) { 234 | Cow::Borrowed(_) => self.clone(), 235 | Cow::Owned(v) => { 236 | #[allow(unused_mut)] 237 | let mut retval = SharedString::new(v); 238 | #[cfg(feature = "track-source")] 239 | retval.set_source_from(self); 240 | retval 241 | } 242 | } 243 | } 244 | } 245 | 246 | impl std::fmt::Display for SharedString { 247 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 248 | write!(f, "{}", &*self.value) 249 | } 250 | } 251 | 252 | impl From<&str> for SharedString { 253 | fn from(value: &str) -> Self { 254 | Self::new(value) 255 | } 256 | } 257 | 258 | pub(crate) fn into_lowercase(string: &str) -> std::borrow::Cow<'_, str> { 259 | // TODO: This requires two iterations over the string, and it's definitely possible 260 | // to do it in one. 261 | if string.chars().all(char::is_lowercase) { 262 | std::borrow::Cow::Borrowed(string) 263 | } else { 264 | std::borrow::Cow::Owned(string.to_lowercase()) 265 | } 266 | } 267 | 268 | /// A parse error and the [`SharedString`] that failed to parse. 269 | #[derive(Clone, Debug)] 270 | pub struct ParseError { 271 | /// The error that occurred during parsing. 272 | pub error: E, 273 | /// The string on which parsing was attempted. 274 | pub string: SharedString, 275 | } 276 | 277 | impl std::fmt::Display for ParseError { 278 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 279 | write!(f, "parse error: {}", &self.error) 280 | } 281 | } 282 | 283 | impl std::error::Error for ParseError { 284 | fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { 285 | Some(&self.error) 286 | } 287 | } 288 | 289 | /// Specialized trait for types whose references can be converted to [`SharedString`]s 290 | /// or possibly [`str`]s. 291 | /// 292 | /// This trait exists to enable faster insertions in data structures that use 293 | /// [`SharedString`]s as keys by avoiding a copy if the key already exists. 294 | pub trait ToSharedString { 295 | /// Converts `self` into a [`SharedString`]. 296 | /// 297 | /// Callers should assume this will involve full copy of `self`'s content, 298 | /// though more efficient implementations are often possible. 299 | fn to_shared_string(self) -> SharedString; 300 | /// Cheaply get a reference to `self`'s data if possible. 301 | /// 302 | /// The returned value, if `Some`, should be equal to the value returned by 303 | /// [`ToSharedString::to_shared_string`]. 304 | fn try_as_str(&self) -> Option<&str> { 305 | None 306 | } 307 | } 308 | 309 | impl ToSharedString for &str { 310 | fn to_shared_string(self) -> SharedString { 311 | SharedString::new(self) 312 | } 313 | 314 | fn try_as_str(&self) -> Option<&str> { 315 | Some(self) 316 | } 317 | } 318 | 319 | impl ToSharedString for String { 320 | fn to_shared_string(self) -> SharedString { 321 | SharedString::new(self.as_str()) 322 | } 323 | 324 | fn try_as_str(&self) -> Option<&str> { 325 | Some(self) 326 | } 327 | } 328 | 329 | impl ToSharedString for SharedString { 330 | fn to_shared_string(self) -> SharedString { 331 | self 332 | } 333 | 334 | fn try_as_str(&self) -> Option<&str> { 335 | Some(self) 336 | } 337 | } 338 | -------------------------------------------------------------------------------- /src/properties.rs: -------------------------------------------------------------------------------- 1 | mod iter; 2 | 3 | pub use iter::*; 4 | 5 | use crate::{ 6 | string::{SharedString, ToSharedString}, 7 | PropertyKey, PropertyValue, 8 | }; 9 | 10 | /// Map of property names to property values. 11 | /// 12 | /// It features O(log n) lookup and preserves insertion order, 13 | /// as well as convenience methods for type-safe access and parsing of values. 14 | /// 15 | /// This structure is case-sensitive. 16 | /// It's the caller's responsibility to ensure all keys and values are lowercased. 17 | #[derive(Clone, Default)] 18 | pub struct Properties { 19 | /// Key-value pairs, ordered from oldest to newest. 20 | pairs: Vec<(SharedString, SharedString)>, 21 | /// Indices of `pairs`, ordered matching the key of the pair each index refers to. 22 | /// This part is what allows logarithmic lookups. 23 | idxes: Vec, 24 | } 25 | 26 | // TODO: Deletion. 27 | 28 | impl Properties { 29 | /// Constructs a new empty [`Properties`]. 30 | pub const fn new() -> Properties { 31 | Properties { 32 | pairs: Vec::new(), 33 | idxes: Vec::new(), 34 | } 35 | } 36 | 37 | /// Returns the number of key-value pairs, including those with empty values. 38 | pub fn len(&self) -> usize { 39 | self.pairs.len() 40 | } 41 | 42 | /// Returns `true` if `self` contains no key-value pairs. 43 | pub fn is_empty(&self) -> bool { 44 | self.pairs.is_empty() 45 | } 46 | 47 | /// Returns either the index of the pair with the desired key in `pairs`, 48 | /// or the index to insert a new index into `index`. 49 | fn find_idx(&self, key: &str) -> Result { 50 | self.idxes 51 | .as_slice() 52 | .binary_search_by_key(&key, |ki| &self.pairs[*ki].0) 53 | .map(|idx| self.idxes[idx]) 54 | } 55 | 56 | /// Returns the unparsed "raw" value for the specified key. 57 | pub fn get_raw_for_key(&self, key: impl AsRef) -> Option<&SharedString> { 58 | self.find_idx(key.as_ref()) 59 | .ok() 60 | .map(|idx| &self.pairs[idx].1) 61 | } 62 | 63 | /// Returns the unparsed "raw" value for the specified property. 64 | pub fn get_raw(&self) -> Option<&SharedString> { 65 | self.get_raw_for_key(T::key()) 66 | } 67 | 68 | /// Returns the parsed value for the specified property. 69 | /// 70 | /// If `self` does not contain a key matching [`T::key()`][PropertyKey::key()], 71 | /// `Err(None)` is returned. Otherwise attempts to parse `T` 72 | /// out of the string value, and returns `Err(Some)` if the parse fails. 73 | pub fn get( 74 | &self, 75 | ) -> Result>> { 76 | // The Option is on the error because all PropertyValues implement Default and the default 77 | // should be assumed if the property is unset, meaning handling invalid values and unset 78 | // values can be done with just one unwrap_or_default. 79 | let Some(raw) = self.get_raw::() else { 80 | return Err(None); 81 | }; 82 | raw.parse::().map_err(|error| { 83 | Some(crate::string::ParseError { 84 | error, 85 | string: raw.clone(), 86 | }) 87 | }) 88 | } 89 | 90 | /// Returns an iterator over the key-value pairs. 91 | /// 92 | /// If the `allow-empty-values` feature is NOT used, 93 | /// key-value pairs where the value is empty will be skipped. 94 | /// Otherwise, they will be returned as normal. 95 | /// 96 | /// Pairs are returned from oldest to newest. 97 | pub fn iter(&self) -> Iter<'_> { 98 | Iter(self.pairs.iter()) 99 | } 100 | 101 | /// Returns an iterator over the key-value pairs that allows mutation of the values. 102 | /// 103 | /// If the `allow-empty-values` feature is NOT used, 104 | /// key-value pairs where the value is empty will be skipped. 105 | /// Otherwise, they will be returned as normal. 106 | /// 107 | /// Pairs are returned from oldest to newest. 108 | pub fn iter_mut(&mut self) -> IterMut<'_> { 109 | IterMut(self.pairs.iter_mut()) 110 | } 111 | 112 | fn get_at_mut(&mut self, idx: usize) -> &mut SharedString { 113 | // PANIC: idx needs to be within the bounds of the pairs vec. 114 | &mut self.pairs.get_mut(idx).unwrap().1 115 | } 116 | 117 | fn insert_at(&mut self, idx: usize, key: SharedString, val: SharedString) { 118 | self.idxes.insert(idx, self.pairs.len()); 119 | self.pairs.push((key, val)); 120 | } 121 | 122 | /// Sets the value for a specified key. 123 | pub fn insert_raw_for_key(&mut self, key: impl ToSharedString, val: impl ToSharedString) { 124 | if let Some(key_str) = key.try_as_str() { 125 | match self.find_idx(key_str) { 126 | Ok(idx) => { 127 | *self.get_at_mut(idx) = val.to_shared_string(); 128 | } 129 | Err(idx) => { 130 | self.insert_at(idx, key.to_shared_string(), val.to_shared_string()); 131 | } 132 | } 133 | } else { 134 | let key = key.to_shared_string(); 135 | match self.find_idx(&key) { 136 | Ok(idx) => { 137 | *self.get_at_mut(idx) = val.to_shared_string(); 138 | } 139 | Err(idx) => { 140 | self.insert_at(idx, key, val.to_shared_string()); 141 | } 142 | } 143 | } 144 | } 145 | 146 | /// Sets the value for a specified property's key. 147 | pub fn insert_raw(&mut self, val: V) { 148 | self.insert_raw_for_key(K::key(), val) 149 | } 150 | 151 | /// Inserts a specified property into the map. 152 | pub fn insert(&mut self, prop: T) { 153 | self.insert_raw_for_key(T::key(), prop) 154 | } 155 | 156 | /// Attempts to add a new key-value pair to the map. 157 | /// 158 | /// If the key was already associated with a value, 159 | /// returns a mutable reference to the old value and does not update the map. 160 | pub fn try_insert_raw_for_key( 161 | &mut self, 162 | key: impl ToSharedString, 163 | val: impl ToSharedString, 164 | ) -> Result<(), &mut SharedString> { 165 | if let Some(key_str) = key.try_as_str() { 166 | match self.find_idx(key_str) { 167 | Ok(idx) => Err(self.get_at_mut(idx)), 168 | Err(idx) => { 169 | self.insert_at(idx, key.to_shared_string(), val.to_shared_string()); 170 | Ok(()) 171 | } 172 | } 173 | } else { 174 | let key = key.to_shared_string(); 175 | match self.find_idx(&key) { 176 | Ok(idx) => Err(self.get_at_mut(idx)), 177 | Err(idx) => { 178 | self.insert_at(idx, key, val.to_shared_string()); 179 | Ok(()) 180 | } 181 | } 182 | } 183 | } 184 | 185 | /// Attempts to add a new property to the map with a specified value. 186 | /// 187 | /// If the key was already associated with a value, 188 | /// returns a mutable reference to the old value and does not update the map. 189 | pub fn try_insert_raw( 190 | &mut self, 191 | val: V, 192 | ) -> Result<(), &mut SharedString> { 193 | self.try_insert_raw_for_key(K::key(), val) 194 | } 195 | 196 | /// Attempts to add a new property to the map. 197 | /// 198 | /// If the key was already associated with a value, 199 | /// returns a mutable reference to the old value and does not update the map. 200 | pub fn try_insert( 201 | &mut self, 202 | prop: T, 203 | ) -> Result<(), &mut SharedString> { 204 | self.try_insert_raw_for_key(T::key(), prop) 205 | } 206 | 207 | /// Adds fallback values for certain common key-value pairs. 208 | /// 209 | /// Used to obtain spec-compliant values for [`crate::property::IndentSize`] 210 | /// and [`crate::property::TabWidth`]. 211 | pub fn use_fallbacks(&mut self) { 212 | crate::fallback::add_fallbacks(self, false) 213 | } 214 | 215 | /// Adds pre-0.9.0 fallback values for certain common key-value pairs. 216 | /// 217 | /// This shouldn't be used outside of narrow cases where 218 | /// compatibility with those older standards is required. 219 | /// Prefer [`Properties::use_fallbacks`] instead. 220 | pub fn use_fallbacks_legacy(&mut self) { 221 | crate::fallback::add_fallbacks(self, true) 222 | } 223 | } 224 | 225 | impl PartialEq for Properties { 226 | fn eq(&self, other: &Self) -> bool { 227 | if self.len() != other.len() { 228 | return false; 229 | } 230 | self.idxes 231 | .iter() 232 | .zip(other.idxes.iter()) 233 | .all(|(idx_s, idx_o)| self.pairs[*idx_s] == other.pairs[*idx_o]) 234 | } 235 | } 236 | 237 | impl Eq for Properties {} 238 | 239 | impl std::fmt::Debug for Properties { 240 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 241 | f.debug_tuple("Properties") 242 | .field(&self.pairs.as_slice()) 243 | .finish() 244 | } 245 | } 246 | 247 | impl<'a> IntoIterator for &'a Properties { 248 | type Item = as Iterator>::Item; 249 | 250 | type IntoIter = Iter<'a>; 251 | 252 | fn into_iter(self) -> Self::IntoIter { 253 | self.iter() 254 | } 255 | } 256 | 257 | impl<'a> IntoIterator for &'a mut Properties { 258 | type Item = as Iterator>::Item; 259 | 260 | type IntoIter = IterMut<'a>; 261 | 262 | fn into_iter(self) -> Self::IntoIter { 263 | self.iter_mut() 264 | } 265 | } 266 | 267 | impl, V: Into> FromIterator<(K, V)> for Properties { 268 | fn from_iter>(iter: T) -> Self { 269 | let mut result = Properties::new(); 270 | result.extend(iter); 271 | result 272 | } 273 | } 274 | 275 | impl, V: Into> Extend<(K, V)> for Properties { 276 | fn extend>(&mut self, iter: T) { 277 | let iter = iter.into_iter(); 278 | let min_len = iter.size_hint().0; 279 | self.pairs.reserve(min_len); 280 | self.idxes.reserve(min_len); 281 | for (k, v) in iter { 282 | let k = k.into(); 283 | let v = v.into(); 284 | self.insert_raw_for_key(k, v); 285 | } 286 | } 287 | } 288 | 289 | /// Trait for types that can add properties to a [`Properties`] map. 290 | pub trait PropertiesSource { 291 | /// Adds properties that apply to a file at the specified path 292 | /// to the provided [`Properties`]. 293 | fn apply_to( 294 | self, 295 | props: &mut Properties, 296 | path: impl AsRef, 297 | ) -> Result<(), crate::Error>; 298 | } 299 | 300 | impl PropertiesSource for &Properties { 301 | fn apply_to( 302 | self, 303 | props: &mut Properties, 304 | _: impl AsRef, 305 | ) -> Result<(), crate::Error> { 306 | props.extend(self.pairs.iter().cloned()); 307 | Ok(()) 308 | } 309 | } 310 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | 204 | --------------------------------------------------------------------------------