├── .clippy.toml ├── .editorconfig ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .gitmodules ├── .rustfmt.toml ├── CHANGELOG.md ├── Cargo.toml ├── DCO.txt ├── LICENSE.txt ├── README.md ├── rustdoc.md ├── src ├── error.rs ├── fallback.rs ├── file.rs ├── glob.rs ├── glob │ ├── flatset.rs │ ├── matcher.rs │ ├── parser.rs │ ├── parser │ │ ├── alt.rs │ │ ├── charclass.rs │ │ ├── main.rs │ │ └── numrange.rs │ ├── splitter.rs │ └── stack.rs ├── lib.rs ├── linereader.rs ├── parser.rs ├── properties.rs ├── properties │ └── iter.rs ├── property.rs ├── rawvalue.rs ├── section.rs ├── tests.rs ├── tests │ ├── ecparser.rs │ ├── glob.rs │ ├── linereader.rs │ ├── properties.rs │ ├── property.rs │ └── version.rs ├── traits.rs └── version.rs └── tools ├── Cargo.toml └── src └── bin └── ec4rs-parse.rs /.clippy.toml: -------------------------------------------------------------------------------- 1 | msrv = "1.56.0" 2 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - main 8 | 9 | jobs: 10 | test: 11 | name: MSRV Library Tests 12 | runs-on: ${{ matrix.os }} 13 | strategy: 14 | matrix: 15 | build: [linux, windows] 16 | include: 17 | - build: linux 18 | os: ubuntu-latest 19 | - build: windows 20 | os: windows-latest 21 | steps: 22 | - name: Checkout 23 | uses: actions/checkout@v3 24 | - name: Install MSRV Rust 25 | uses: actions-rs/toolchain@v1 26 | with: 27 | profile: minimal 28 | toolchain: 1.56.0 29 | - name: Build 30 | uses: actions-rs/cargo@v1 31 | with: 32 | command: build 33 | - name: Unit Tests (default features) 34 | uses: actions-rs/cargo@v1 35 | with: 36 | command: test 37 | - name: "Unit Tests (all features)" 38 | uses: actions-rs/cargo@v1 39 | with: 40 | command: test 41 | args: "--all-features" 42 | quality: 43 | name: Code Quality 44 | runs-on: ubuntu-latest 45 | steps: 46 | - name: Checkout 47 | uses: actions/checkout@v3 48 | - name: Install Rust 49 | uses: actions-rs/toolchain@v1 50 | with: 51 | profile: default 52 | toolchain: stable 53 | - name: Check Clippy 54 | uses: actions-rs/cargo@v1 55 | with: 56 | command: clippy 57 | - name: Check Docs 58 | uses: actions-rs/cargo@v1 59 | with: 60 | command: doc 61 | args: "--no-deps" 62 | - name: Check Style 63 | uses: actions-rs/cargo@v1 64 | with: 65 | command: fmt 66 | args: "--check" 67 | compliance: 68 | name: Binary Tests 69 | runs-on: ubuntu-latest 70 | steps: 71 | - name: Checkout 72 | uses: actions/checkout@v3 73 | with: 74 | submodules: true 75 | - name: Install Rust 76 | uses: actions-rs/toolchain@v1 77 | with: 78 | profile: minimal 79 | toolchain: stable 80 | - name: Build 81 | uses: actions-rs/cargo@v1 82 | with: 83 | command: build 84 | args: "-p ec4rs_tools" 85 | - name: Install CMake 86 | uses: jwlawson/actions-setup-cmake@v1.12 87 | - name: Core Tests 88 | shell: bash 89 | run: | 90 | cd tests 91 | cmake -DEDITORCONFIG_CMD="$PWD/../target/debug/ec4rs-parse" . 92 | ctest . 93 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock 3 | Testing 4 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "tests"] 2 | path = tests 3 | url = https://github.com/editorconfig/editorconfig-core-test.git 4 | -------------------------------------------------------------------------------- /.rustfmt.toml: -------------------------------------------------------------------------------- 1 | max_width = 100 2 | use_field_init_shorthand = true 3 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 = ["tools"] 21 | 22 | [features] 23 | allow-empty-values = [] 24 | track-source = [] 25 | 26 | [dependencies] 27 | language-tags = { version = "0.3.2", optional = true } 28 | 29 | [package.metadata.docs.rs] 30 | all-features = true 31 | rustdoc-args = ["--cfg", "doc_unstable"] 32 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ec4rs: EditorConfig For Rust 2 | [![CI](https://github.com/TheDaemoness/ec4rs/actions/workflows/ci.yml/badge.svg)](https://github.com/TheDaemoness/ec4rs/actions/workflows/ci.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 | The main repository for this library includes the EditorConfig 27 | [core tests](https://github.com/editorconfig/editorconfig-core-test) 28 | as a Git submodule. This library should pass all of these tests. 29 | To run the test suite, run the following commands in a POSIX-like shell: 30 | 31 | ```bash 32 | cargo build --package ec4rs_tools 33 | git submodule update --init --recursive 34 | cd tests 35 | cmake -DEDITORCONFIG_CMD="$PWD/../target/debug/ec4rs-parse" . 36 | ctest . 37 | ``` 38 | 39 | ## License 40 | 41 | **ec4rs** is licensed under the 42 | [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0.html) 43 | with no `NOTICE` text. 44 | 45 | Contributors submitting code changes must agree to the terms of the 46 | [Developer Certificate of Origin (DCO)](https://developercertificate.org/) 47 | to have their contributions accepted for inclusion. 48 | A copy of the DCO may be found in `DCO.txt`. 49 | Contributors should sign-off on their commits (see `git commit -s`) 50 | to indicate explicit agreement. 51 | -------------------------------------------------------------------------------- /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").unwrap_or_default(); 28 | // Convenient access to ec4rs's property parsers. 29 | use ec4rs::property::*; 30 | // Use fallback values for tab width and/or indent size. 31 | cfg.use_fallbacks(); 32 | 33 | // Let ec4rs do the parsing for you. 34 | let indent_style: IndentStyle = cfg.get::() 35 | .unwrap_or(IndentStyle::Tabs); 36 | 37 | // Get a string value, with a default. 38 | let charset: &str = cfg.get_raw::() 39 | .filter_unset() // Handle the special "unset" value. 40 | .into_option() 41 | .unwrap_or("utf-8"); 42 | 43 | // Parse a non-standard property. 44 | let hard_wrap = cfg.get_raw_for_key("max_line_length") 45 | .into_str() 46 | .parse::(); 47 | ``` 48 | 49 | ## Features 50 | 51 | **allow-empty-values**: Consider lines with a key but no value as valid. 52 | This is likely to be explicitly allowed in a future version of the 53 | EditorConfig specification, but `ec4rs` currently by default treats such lines 54 | as invalid, necessitating this feature flag to reduce behavioral breakage. 55 | 56 | **language-tags**: Use the `language-tags` crate, which adds parsing for the 57 | `spelling_language` property. 58 | 59 | **track-source**: Allow [`RawValue`][crate::rawvalue::RawValue] 60 | to store the file and line number it originates from. 61 | [`ConfigParser`] will add this information where applicable. 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 | /// An empty character class was found in a section header. 11 | EmptyCharClass, 12 | } 13 | 14 | impl std::fmt::Display for ParseError { 15 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 16 | match self { 17 | ParseError::Eof => write!(f, "end of data"), 18 | ParseError::Io(e) => write!(f, "io failure: {}", e), 19 | ParseError::InvalidLine => write!(f, "invalid line"), 20 | ParseError::EmptyCharClass => write!(f, "empty char class"), 21 | } 22 | } 23 | } 24 | 25 | impl std::error::Error for ParseError { 26 | fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { 27 | if let ParseError::Io(ioe) = self { 28 | Some(ioe) 29 | } else { 30 | None 31 | } 32 | } 33 | } 34 | 35 | /// All errors that can occur during operation. 36 | #[derive(Debug)] 37 | pub enum Error { 38 | /// An error occured durign parsing. 39 | Parse(ParseError), 40 | /// An error occured during parsing of a file. 41 | InFile(std::path::PathBuf, usize, ParseError), 42 | /// The current working directory is invalid (e.g. does not exist). 43 | InvalidCwd(std::io::Error), 44 | } 45 | 46 | impl std::fmt::Display for Error { 47 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 48 | match self { 49 | Error::Parse(error) => write!(f, "{}", error), 50 | Error::InFile(path, line, error) => { 51 | write!(f, "{}:{}: {}", path.to_string_lossy(), line, error) 52 | } 53 | Error::InvalidCwd(ioe) => write!(f, "invalid cwd: {}", ioe), 54 | } 55 | } 56 | } 57 | 58 | impl std::error::Error for Error { 59 | fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { 60 | match self { 61 | Error::Parse(pe) | Error::InFile(_, _, pe) => pe.source(), 62 | Error::InvalidCwd(ioe) => Some(ioe), 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /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.into_option() { 6 | if let Ok(prop::IndentSize::UseTabWidth) = val.parse::() { 7 | let value = props 8 | .get_raw::() 9 | .into_option() 10 | .unwrap_or("tab") 11 | .to_owned(); 12 | props.insert_raw::(value); 13 | } else { 14 | let value = value.to_owned(); 15 | let _ = props.try_insert_raw::(value); 16 | } 17 | } else if let Some(value) = props 18 | .get_raw::() 19 | .filter_unset() 20 | .into_option() 21 | { 22 | let _ = props.try_insert_raw::(value.to_owned()); 23 | } 24 | if !legacy { 25 | if let Ok(prop::IndentStyle::Tabs) = props.get::() { 26 | let _ = props.try_insert(prop::IndentSize::UseTabWidth); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/file.rs: -------------------------------------------------------------------------------- 1 | use std::path::{Path, PathBuf}; 2 | 3 | use crate::{ConfigParser, Error, ParseError, Properties, PropertiesSource, Section}; 4 | 5 | /// Convenience wrapper for an [`ConfigParser`] that reads files. 6 | pub struct ConfigFile { 7 | // TODO: Arc. It's more important to have cheap clones than mutability. 8 | /// The path to the open file. 9 | pub path: PathBuf, 10 | /// A [`ConfigParser`] that reads from the file. 11 | pub reader: ConfigParser>, 12 | } 13 | 14 | impl ConfigFile { 15 | /// Opens a file for reading and uses it to construct an [`ConfigParser`]. 16 | /// 17 | /// If the file cannot be opened, wraps the [`std::io::Error`] in a [`ParseError`]. 18 | pub fn open(path: impl Into) -> Result { 19 | let path = path.into(); 20 | let file = std::fs::File::open(&path).map_err(ParseError::Io)?; 21 | let reader = ConfigParser::new_buffered_with_path(file, Some(path.as_ref()))?; 22 | Ok(ConfigFile { path, reader }) 23 | } 24 | 25 | /// Wraps a [`ParseError`] in an [`Error::InFile`]. 26 | /// 27 | /// Uses the path and current line number from this instance. 28 | pub fn add_error_context(&self, error: ParseError) -> Error { 29 | Error::InFile(self.path.clone(), self.reader.line_no(), error) 30 | } 31 | } 32 | 33 | impl Iterator for ConfigFile { 34 | type Item = Result; 35 | fn next(&mut self) -> Option { 36 | self.reader.next() 37 | } 38 | } 39 | 40 | impl std::iter::FusedIterator for ConfigFile {} 41 | 42 | impl PropertiesSource for &mut ConfigFile { 43 | /// Adds properties from the file's sections to the specified [`Properties`] map. 44 | /// 45 | /// Uses [`ConfigFile::path`] when determining applicability to stop `**` from going too far. 46 | /// Returns parse errors wrapped in an [`Error::InFile`]. 47 | fn apply_to(self, props: &mut Properties, path: impl AsRef) -> Result<(), crate::Error> { 48 | let get_parent = || self.path.parent(); 49 | let path = if let Some(parent) = get_parent() { 50 | let path = path.as_ref(); 51 | path.strip_prefix(parent).unwrap_or(path) 52 | } else { 53 | path.as_ref() 54 | }; 55 | match self.reader.apply_to(props, path) { 56 | Ok(()) => Ok(()), 57 | Err(crate::Error::Parse(e)) => Err(self.add_error_context(e)), 58 | Err(e) => panic!("unexpected error variant {:?}", e), 59 | } 60 | } 61 | } 62 | 63 | /// Directory traverser for finding and opening EditorConfig files. 64 | /// 65 | /// All the contained files are open for reading and have not had any sections read. 66 | /// When iterated over, either by using it as an [`Iterator`] 67 | /// or by calling [`ConfigFiles::iter`], 68 | /// returns [`ConfigFile`]s in the order that they would apply to a [`Properties`] map. 69 | pub struct ConfigFiles(Vec); 70 | 71 | impl ConfigFiles { 72 | /// Searches for EditorConfig files that might apply to a file at the specified path. 73 | /// 74 | /// This function does not canonicalize the path, 75 | /// but will join relative paths onto the current working directory. 76 | /// 77 | /// EditorConfig files are assumed to be named `.editorconfig` 78 | /// unless an override is supplied as the second argument. 79 | #[allow(clippy::needless_pass_by_value)] 80 | pub fn open( 81 | path: impl AsRef, 82 | config_path_override: Option>, 83 | ) -> Result { 84 | use std::borrow::Cow; 85 | let filename = config_path_override 86 | .as_ref() 87 | .map_or_else(|| ".editorconfig".as_ref(), |f| f.as_ref()); 88 | Ok(ConfigFiles(if filename.is_relative() { 89 | let mut abs_path = Cow::from(path.as_ref()); 90 | if abs_path.is_relative() { 91 | abs_path = std::env::current_dir() 92 | .map_err(Error::InvalidCwd)? 93 | .join(&path) 94 | .into() 95 | } 96 | let mut path = abs_path.as_ref(); 97 | let mut vec = Vec::new(); 98 | while let Some(dir) = path.parent() { 99 | if let Ok(file) = ConfigFile::open(dir.join(filename)) { 100 | let should_break = file.reader.is_root; 101 | vec.push(file); 102 | if should_break { 103 | break; 104 | } 105 | } 106 | path = dir; 107 | } 108 | vec 109 | } else { 110 | // TODO: Better errors. 111 | vec![ConfigFile::open(filename).map_err(Error::Parse)?] 112 | })) 113 | } 114 | 115 | /// Returns an iterator over the contained [`ConfigFiles`]. 116 | pub fn iter(&self) -> impl Iterator { 117 | self.0.iter().rev() 118 | } 119 | 120 | // To maintain the invariant that these files have not had any sections read, 121 | // there is no `iter_mut` method. 122 | } 123 | 124 | impl Iterator for ConfigFiles { 125 | type Item = ConfigFile; 126 | fn next(&mut self) -> Option { 127 | self.0.pop() 128 | } 129 | } 130 | 131 | impl std::iter::FusedIterator for ConfigFiles {} 132 | 133 | impl PropertiesSource for ConfigFiles { 134 | /// Adds properties from the files' sections to the specified [`Properties`] map. 135 | /// 136 | /// Ignores the files' paths when determining applicability. 137 | fn apply_to(self, props: &mut Properties, path: impl AsRef) -> Result<(), crate::Error> { 138 | let path = path.as_ref(); 139 | for mut file in self { 140 | file.apply_to(props, path)?; 141 | } 142 | Ok(()) 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /src/glob.rs: -------------------------------------------------------------------------------- 1 | // TODO: All of this glob stuff should be extracted to its own crate. 2 | 3 | mod flatset; 4 | mod matcher; 5 | mod parser; 6 | mod splitter; 7 | mod stack; 8 | 9 | pub use matcher::Matcher; 10 | 11 | use flatset::FlatSet; 12 | use splitter::Splitter; 13 | 14 | #[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug)] 15 | pub struct Glob(pub(super) Vec); 16 | 17 | impl Glob { 18 | pub fn new(pattern: &str) -> Glob { 19 | parser::parse(pattern) 20 | } 21 | 22 | #[must_use] 23 | pub fn matches(&self, path: &std::path::Path) -> bool { 24 | matcher::matches(path, self).is_some() 25 | } 26 | 27 | pub(super) fn append_char(&mut self, c: char) { 28 | if c == '/' { 29 | self.append(Matcher::Sep); 30 | } else if let Some(Matcher::Suffix(string)) = self.0.last_mut() { 31 | string.push(c); 32 | } else { 33 | // Since we know the Matcher::Suffix case in append() will always be false, 34 | // we can just save the optimizer the trouble. 35 | self.0.push(Matcher::Suffix(c.to_string())); 36 | } 37 | } 38 | 39 | #[inline] 40 | pub(super) fn append(&mut self, matcher: Matcher) { 41 | // Optimizations, fusing certain kinds of matchers together. 42 | let push = !match &matcher { 43 | Matcher::Sep => { 44 | matches!(&self.0.last(), Some(Matcher::Sep)) 45 | } 46 | Matcher::Suffix(suffix) => { 47 | if let Some(Matcher::Suffix(prefix)) = self.0.last_mut() { 48 | prefix.push_str(suffix); 49 | true 50 | } else { 51 | false 52 | } 53 | } 54 | Matcher::AnySeq(true) => { 55 | matches!(&self.0.last(), Some(Matcher::AnySeq(false))) 56 | } 57 | _ => false, 58 | }; 59 | if push { 60 | self.0.push(matcher); 61 | } 62 | } 63 | 64 | pub fn append_glob(&mut self, glob: Glob) { 65 | for matcher in glob.0 { 66 | self.append(matcher) 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/glob/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 | -------------------------------------------------------------------------------- /src/glob/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 | -------------------------------------------------------------------------------- /src/glob/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 | -------------------------------------------------------------------------------- /src/glob/parser/alt.rs: -------------------------------------------------------------------------------- 1 | use crate::glob::{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_char('{'); 69 | self.glob.append_char('}'); 70 | self.glob 71 | } 72 | 1 => { 73 | self.glob.append_char('{'); 74 | for matcher in self.options.pop().unwrap().0 { 75 | self.glob.append(matcher); 76 | } 77 | self.glob.append_char('}'); 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.append(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_char('{'); 92 | for option in self.options { 93 | if first { 94 | first = false; 95 | } else { 96 | self.glob.append_char(',') 97 | } 98 | self.glob.append_glob(option); 99 | } 100 | self.glob 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/glob/parser/charclass.rs: -------------------------------------------------------------------------------- 1 | use std::collections::BTreeSet; 2 | 3 | use super::Chars; 4 | use crate::glob::{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_char('['); 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.append(Matcher::AnyChar); 61 | } else { 62 | glob.append_char('['); 63 | glob.append_char(']'); 64 | } 65 | } 66 | // Don't use BTreeSet::first here (stable: 1.66). 67 | 1 => glob.append_char(*charclass.iter().next().unwrap()), 68 | _ => glob.append(Matcher::CharClass(charclass.into(), !invert)), 69 | } 70 | (glob, chars) 71 | } else { 72 | glob.append_char('['); 73 | (glob, restore) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/glob/parser/main.rs: -------------------------------------------------------------------------------- 1 | use super::alt::AltStack; 2 | use crate::glob::{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_char('/'); 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_char(escaped); 15 | } 16 | } 17 | '?' => retval.append(Matcher::AnyChar), 18 | '*' => retval.append(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.append(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_char(','); 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_char('}'); 52 | } 53 | } 54 | _ => retval.append_char(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.append(Matcher::AnySeq(false)); 70 | } 71 | retval 72 | } 73 | -------------------------------------------------------------------------------- /src/glob/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 | -------------------------------------------------------------------------------- /src/glob/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/glob/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 | -------------------------------------------------------------------------------- /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::missing_errors_doc)] // reason = "TODO: Fix." 21 | #![cfg_attr(doc_unstable, feature(doc_auto_cfg))] 22 | 23 | mod error; 24 | mod fallback; 25 | mod file; 26 | mod glob; 27 | mod linereader; 28 | mod parser; 29 | mod properties; 30 | pub mod property; 31 | pub mod rawvalue; 32 | mod section; 33 | #[cfg(test)] 34 | mod tests; 35 | mod traits; 36 | pub mod version; 37 | 38 | pub use error::{Error, ParseError}; 39 | pub use file::{ConfigFile, ConfigFiles}; 40 | pub use parser::ConfigParser; 41 | pub use properties::{Properties, PropertiesSource}; 42 | pub use section::Section; 43 | pub use traits::*; 44 | 45 | #[cfg(feature = "language-tags")] 46 | pub mod language_tags { 47 | //! Re-export of the `language-tags` crate. 48 | pub use ::language_tags::*; 49 | } 50 | 51 | /// Retrieves the [`Properties`] for a file at the given path. 52 | /// 53 | /// This is the simplest way to use this library in an EditorConfig integration or plugin. 54 | /// 55 | /// This function does not canonicalize the path, 56 | /// but will join relative paths onto the current working directory. 57 | /// 58 | /// EditorConfig files are assumed to be named `.editorconfig`. 59 | /// If not, use [`properties_from_config_of`] 60 | pub fn properties_of(path: impl AsRef) -> Result { 61 | properties_from_config_of(path, Option::<&std::path::Path>::None) 62 | } 63 | 64 | /// Retrieves the [`Properties`] for a file at the given path, 65 | /// expecting EditorConfig files to be named matching `config_path_override`. 66 | /// 67 | /// This function does not canonicalize the path, 68 | /// but will join relative paths onto the current working directory. 69 | /// 70 | /// If the provided config path is absolute, uses the EditorConfig file at that path. 71 | /// If it's relative, joins it onto every ancestor of the target file, 72 | /// and looks for config files at those paths. 73 | /// If it's `None`, EditorConfig files are assumed to be named `.editorconfig`. 74 | pub fn properties_from_config_of( 75 | target_path: impl AsRef, 76 | config_path_override: Option>, 77 | ) -> Result { 78 | let mut retval = Properties::new(); 79 | ConfigFiles::open(&target_path, config_path_override)?.apply_to(&mut retval, &target_path)?; 80 | Ok(retval) 81 | } 82 | -------------------------------------------------------------------------------- /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) => { 57 | #[cfg(feature = "allow-empty-values")] 58 | { 59 | Ok(Line::Pair(key.trim_end(), val)) 60 | } 61 | #[cfg(not(feature = "allow-empty-values"))] 62 | { 63 | Err(ParseError::InvalidLine) 64 | } 65 | } 66 | (false, false) => Ok(Line::Pair(key.trim_end(), val.trim_start())), 67 | } 68 | } else { 69 | Err(ParseError::InvalidLine) 70 | } 71 | } 72 | 73 | /// Struct for extracting valid INI-like lines from text, 74 | /// suitable for initial parsing of individual .editorconfig files. 75 | /// Does minimal validation and does not modify the input text in any way. 76 | pub struct LineReader { 77 | ticker: usize, 78 | line: String, 79 | reader: R, 80 | } 81 | 82 | impl LineReader { 83 | /// Constructs a new line reader. 84 | pub fn new(r: R) -> LineReader { 85 | LineReader { 86 | ticker: 0, 87 | line: String::with_capacity(256), 88 | reader: r, 89 | } 90 | } 91 | 92 | /// Returns the line number of the contained line. 93 | pub fn line_no(&self) -> usize { 94 | self.ticker 95 | } 96 | 97 | /// Returns a reference to the contained line. 98 | pub fn line(&self) -> &str { 99 | self.line.as_str() 100 | } 101 | 102 | /// Parses the contained line using [`parse_line`]. 103 | /// 104 | /// It's usually not necessary to call this method. 105 | /// See [`LineReader::next`]. 106 | pub fn reparse(&self) -> LineReadResult<'_> { 107 | parse_line(self.line()) 108 | } 109 | 110 | /// Reads and parses the next line from the stream. 111 | pub fn next_line(&mut self) -> LineReadResult<'_> { 112 | self.line.clear(); 113 | match self.reader.read_line(&mut self.line) { 114 | Err(e) => Err(ParseError::Io(e)), 115 | Ok(0) => Err(ParseError::Eof), 116 | Ok(_) => { 117 | self.ticker += 1; 118 | if self.ticker == 1 { 119 | parse_line(self.line.strip_prefix('\u{FEFF}').unwrap_or(&self.line)) 120 | } else { 121 | self.reparse() 122 | } 123 | } 124 | } 125 | } 126 | } 127 | 128 | fn is_comment(c: char) -> bool { 129 | c == ';' || c == '#' 130 | } 131 | -------------------------------------------------------------------------------- /src/parser.rs: -------------------------------------------------------------------------------- 1 | use crate::linereader::LineReader; 2 | use crate::ParseError; 3 | use crate::Section; 4 | use std::io; 5 | use std::path::Path; 6 | 7 | /// Parser for the text of an EditorConfig file. 8 | /// 9 | /// This struct wraps any [`BufRead`][std::io::BufRead]. 10 | /// It eagerly parses the preamble on construction. 11 | /// [`Section`]s may then be parsed by calling [`ConfigParser::read_section`]. 12 | pub struct ConfigParser { 13 | /// Incidates if a `root = true` line was found in the preamble. 14 | pub is_root: bool, 15 | eof: bool, 16 | reader: LineReader, 17 | #[cfg(feature = "track-source")] 18 | path: Option>, 19 | } 20 | 21 | impl ConfigParser> { 22 | /// Convenience function for construction using an unbuffered [`io::Read`]. 23 | /// 24 | /// See [`ConfigParser::new`]. 25 | pub fn new_buffered(source: R) -> Result>, ParseError> { 26 | Self::new(io::BufReader::new(source)) 27 | } 28 | /// Convenience function for construction using an unbuffered [`io::Read`] 29 | /// which is assumed to be a file at `path`. 30 | /// 31 | /// See [`ConfigParser::new_with_path`]. 32 | pub fn new_buffered_with_path( 33 | source: R, 34 | path: Option>>, 35 | ) -> Result>, ParseError> { 36 | Self::new_with_path(io::BufReader::new(source), path) 37 | } 38 | } 39 | 40 | impl ConfigParser { 41 | /// Constructs a new [`ConfigParser`] and reads the preamble from the provided source, 42 | /// which is assumed to be a file at `path`. 43 | /// 44 | /// Returns `Ok` if the preamble was parsed successfully, 45 | /// otherwise returns `Err` with the error that occurred during reading. 46 | /// 47 | /// If the `track-source` feature is enabled and `path` is `Some`, 48 | /// [`RawValue`][crate::rawvalue::RawValue]s produced by this parser will 49 | /// have their sources set appropriately. 50 | /// Otherwise, `path` is unused. 51 | pub fn new_with_path( 52 | buf_source: R, 53 | #[allow(unused)] path: Option>>, 54 | ) -> Result, ParseError> { 55 | let mut reader = LineReader::new(buf_source); 56 | let mut is_root = false; 57 | let eof = loop { 58 | use crate::linereader::Line; 59 | match reader.next_line() { 60 | Err(ParseError::Eof) => break true, 61 | Err(e) => return Err(e), 62 | Ok(Line::Nothing) => (), 63 | Ok(Line::Section(_)) => break false, 64 | Ok(Line::Pair(k, v)) => { 65 | if "root".eq_ignore_ascii_case(k) { 66 | if let Ok(b) = v.to_ascii_lowercase().parse::() { 67 | is_root = b; 68 | } 69 | } 70 | // Quietly ignore unknown properties. 71 | } 72 | } 73 | }; 74 | #[cfg(feature = "track-source")] 75 | let path = path.map(Into::into); 76 | Ok(ConfigParser { 77 | is_root, 78 | eof, 79 | reader, 80 | #[cfg(feature = "track-source")] 81 | path, 82 | }) 83 | } 84 | /// Constructs a new [`ConfigParser`] and reads the preamble from the provided source. 85 | /// 86 | /// Returns `Ok` if the preamble was parsed successfully, 87 | /// otherwise returns `Err` with the error that occurred during reading. 88 | pub fn new(buf_source: R) -> Result, ParseError> { 89 | Self::new_with_path(buf_source, Option::>::None) 90 | } 91 | 92 | /// Returns `true` if there may be another section to read. 93 | pub fn has_more(&self) -> bool { 94 | self.eof 95 | } 96 | 97 | /// Returns the current line number. 98 | pub fn line_no(&self) -> usize { 99 | self.reader.line_no() 100 | } 101 | 102 | /// Parses a [`Section`], reading more if needed. 103 | pub fn read_section(&mut self) -> Result { 104 | use crate::linereader::Line; 105 | if self.eof { 106 | return Err(ParseError::Eof); 107 | } 108 | if let Ok(Line::Section(header)) = self.reader.reparse() { 109 | let mut section = Section::new(header); 110 | loop { 111 | // Get line_no here to avoid borrowing issues, increment for 1-based indices. 112 | #[cfg(feature = "track-source")] 113 | let line_no = self.reader.line_no() + 1; 114 | match self.reader.next_line() { 115 | Err(e) => { 116 | self.eof = true; 117 | break if matches!(e, ParseError::Eof) { 118 | Ok(section) 119 | } else { 120 | Err(e) 121 | }; 122 | } 123 | Ok(Line::Section(_)) => break Ok(section), 124 | Ok(Line::Nothing) => (), 125 | Ok(Line::Pair(k, v)) => { 126 | #[allow(unused_mut)] 127 | let mut v = crate::rawvalue::RawValue::from(v.to_owned()); 128 | #[cfg(feature = "track-source")] 129 | if let Some(path) = self.path.as_ref() { 130 | v.set_source(path.clone(), line_no); 131 | } 132 | section.insert(k, v); 133 | } 134 | } 135 | } 136 | } else { 137 | Err(ParseError::InvalidLine) 138 | } 139 | } 140 | } 141 | 142 | impl Iterator for ConfigParser { 143 | type Item = Result; 144 | fn next(&mut self) -> Option { 145 | match self.read_section() { 146 | Ok(r) => Some(Ok(r)), 147 | Err(ParseError::Eof) => None, 148 | Err(e) => Some(Err(e)), 149 | } 150 | } 151 | } 152 | 153 | impl std::iter::FusedIterator for ConfigParser {} 154 | 155 | impl crate::PropertiesSource for &mut ConfigParser { 156 | fn apply_to( 157 | self, 158 | props: &mut crate::Properties, 159 | path: impl AsRef, 160 | ) -> Result<(), crate::Error> { 161 | let path = path.as_ref(); 162 | for section_result in self { 163 | match section_result { 164 | Ok(section) => { 165 | let _ = section.apply_to(props, path); 166 | } 167 | Err(error) => return Err(crate::Error::Parse(error)), 168 | } 169 | } 170 | Ok(()) 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /src/properties.rs: -------------------------------------------------------------------------------- 1 | mod iter; 2 | 3 | pub use iter::*; 4 | 5 | use crate::rawvalue::RawValue; 6 | use crate::{PropertyKey, PropertyValue}; 7 | 8 | /// Map of property names to property values. 9 | /// 10 | /// It features O(log n) lookup and preserves insertion order, 11 | /// as well as convenience methods for type-safe access and parsing of values. 12 | /// 13 | /// This structure is case-sensitive. 14 | /// It's the caller's responsibility to ensure all keys and values are lowercased. 15 | #[derive(Clone, Default)] 16 | pub struct Properties { 17 | // Don't use Cow<'static, str> here because it's actually less-optimal 18 | // for the vastly more-common case of reading parsed properties. 19 | // It's a micro-optimization anyway. 20 | /// Key-value pairs, ordered from oldest to newest. 21 | pairs: Vec<(String, RawValue)>, 22 | /// Indices of `pairs`, ordered matching the key of the pair each index refers to. 23 | /// This part is what allows logarithmic lookups. 24 | idxes: Vec, 25 | // Unfortunately, we hand out `&mut RawValue`s all over the place, 26 | // so "no empty RawValues in Properties" cannot be made an invariant 27 | // without breaking API changes. 28 | } 29 | 30 | // TODO: Deletion. 31 | 32 | impl Properties { 33 | /// Constructs a new empty [`Properties`]. 34 | pub const fn new() -> Properties { 35 | Properties { 36 | pairs: Vec::new(), 37 | idxes: Vec::new(), 38 | } 39 | } 40 | 41 | /// Returns the number of key-value pairs, including those with empty values. 42 | pub fn len(&self) -> usize { 43 | self.pairs.len() 44 | } 45 | 46 | /// Returns `true` if `self` contains no key-value pairs. 47 | pub fn is_empty(&self) -> bool { 48 | self.pairs.is_empty() 49 | } 50 | 51 | /// Returns either the index of the pair with the desired key in `pairs`, 52 | /// or the index to insert a new index into `index`. 53 | fn find_idx(&self, key: &str) -> Result { 54 | self.idxes 55 | .as_slice() 56 | .binary_search_by_key(&key, |ki| self.pairs[*ki].0.as_str()) 57 | .map(|idx| self.idxes[idx]) 58 | } 59 | 60 | /// Returns the unparsed "raw" value for the specified key. 61 | /// 62 | /// Does not test for the "unset" value. Use [`RawValue::filter_unset`]. 63 | pub fn get_raw_for_key(&self, key: impl AsRef) -> &RawValue { 64 | self.find_idx(key.as_ref()) 65 | .ok() 66 | .map_or(&crate::rawvalue::UNSET, |idx| &self.pairs[idx].1) 67 | } 68 | 69 | /// Returns the unparsed "raw" value for the specified property. 70 | /// 71 | /// Does not test for the "unset" value. Use [`RawValue::filter_unset`]. 72 | pub fn get_raw(&self) -> &RawValue { 73 | self.get_raw_for_key(T::key()) 74 | } 75 | 76 | /// Returns the parsed value for the specified property. 77 | /// 78 | /// Does not test for the "unset" value if parsing fails. Use [`RawValue::filter_unset`]. 79 | pub fn get(&self) -> Result { 80 | let retval = self.get_raw::(); 81 | retval.parse::().or(Err(retval)) 82 | } 83 | 84 | /// Returns an iterator over the key-value pairs. 85 | /// 86 | /// If the `allow-empty-values` feature is NOT used, 87 | /// key-value pairs where the value is empty will be skipped. 88 | /// Otherwise, they will be returned as normal. 89 | /// 90 | /// Pairs are returned from oldest to newest. 91 | pub fn iter(&self) -> Iter<'_> { 92 | Iter(self.pairs.iter()) 93 | } 94 | 95 | /// Returns an iterator over the key-value pairs that allows mutation of the values. 96 | /// 97 | /// If the `allow-empty-values` feature is NOT used, 98 | /// key-value pairs where the value is empty will be skipped. 99 | /// Otherwise, they will be returned as normal. 100 | /// 101 | /// Pairs are returned from oldest to newest. 102 | pub fn iter_mut(&mut self) -> IterMut<'_> { 103 | IterMut(self.pairs.iter_mut()) 104 | } 105 | 106 | fn get_at_mut(&mut self, idx: usize) -> &mut RawValue { 107 | &mut self.pairs.get_mut(idx).unwrap().1 108 | } 109 | 110 | fn insert_at(&mut self, idx: usize, key: String, val: RawValue) { 111 | self.idxes.insert(idx, self.pairs.len()); 112 | self.pairs.push((key, val)); 113 | } 114 | 115 | /// Sets the value for a specified key. 116 | pub fn insert_raw_for_key(&mut self, key: impl AsRef, val: impl Into) { 117 | let key_str = key.as_ref(); 118 | match self.find_idx(key_str) { 119 | Ok(idx) => { 120 | *self.get_at_mut(idx) = val.into(); 121 | } 122 | Err(idx) => { 123 | self.insert_at(idx, key_str.to_owned(), val.into()); 124 | } 125 | } 126 | } 127 | 128 | /// Sets the value for a specified property's key. 129 | pub fn insert_raw>(&mut self, val: V) { 130 | self.insert_raw_for_key(K::key(), val) 131 | } 132 | 133 | /// Inserts a specified property into the map. 134 | pub fn insert>(&mut self, prop: T) { 135 | self.insert_raw_for_key(T::key(), prop.into()) 136 | } 137 | 138 | /// Attempts to add a new key-value pair to the map. 139 | /// 140 | /// If the key was already associated with a value, 141 | /// returns a mutable reference to the old value and does not update the map. 142 | pub fn try_insert_raw_for_key( 143 | &mut self, 144 | key: impl AsRef, 145 | value: impl Into, 146 | ) -> Result<(), &mut RawValue> { 147 | let key_str = key.as_ref(); 148 | #[allow(clippy::unit_arg)] 149 | match self.find_idx(key_str) { 150 | Ok(idx) => { 151 | let valref = self.get_at_mut(idx); 152 | if valref.is_unset() { 153 | *valref = value.into(); 154 | Ok(()) 155 | } else { 156 | Err(valref) 157 | } 158 | } 159 | Err(idx) => Ok(self.insert_at(idx, key_str.to_owned(), value.into())), 160 | } 161 | } 162 | 163 | /// Attempts to add a new property to the map with a specified value. 164 | /// 165 | /// If the key was already associated with a value, 166 | /// returns a mutable reference to the old value and does not update the map. 167 | pub fn try_insert_raw>( 168 | &mut self, 169 | val: V, 170 | ) -> Result<(), &mut RawValue> { 171 | self.try_insert_raw_for_key(K::key(), val) 172 | } 173 | 174 | /// Attempts to add a new property to the map. 175 | /// 176 | /// If the key was already associated with a value, 177 | /// returns a mutable reference to the old value and does not update the map. 178 | pub fn try_insert>( 179 | &mut self, 180 | prop: T, 181 | ) -> Result<(), &mut RawValue> { 182 | self.try_insert_raw_for_key(T::key(), prop.into()) 183 | } 184 | 185 | /// Adds fallback values for certain common key-value pairs. 186 | /// 187 | /// Used to obtain spec-compliant values for [`crate::property::IndentSize`] 188 | /// and [`crate::property::TabWidth`]. 189 | pub fn use_fallbacks(&mut self) { 190 | crate::fallback::add_fallbacks(self, false) 191 | } 192 | 193 | /// Adds pre-0.9.0 fallback values for certain common key-value pairs. 194 | /// 195 | /// This shouldn't be used outside of narrow cases where 196 | /// compatibility with those older standards is required. 197 | /// Prefer [`Properties::use_fallbacks`] instead. 198 | pub fn use_fallbacks_legacy(&mut self) { 199 | crate::fallback::add_fallbacks(self, true) 200 | } 201 | } 202 | 203 | impl PartialEq for Properties { 204 | fn eq(&self, other: &Self) -> bool { 205 | if self.len() != other.len() { 206 | return false; 207 | } 208 | self.idxes 209 | .iter() 210 | .zip(other.idxes.iter()) 211 | .all(|(idx_s, idx_o)| self.pairs[*idx_s] == other.pairs[*idx_o]) 212 | } 213 | } 214 | 215 | impl Eq for Properties {} 216 | 217 | impl std::fmt::Debug for Properties { 218 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 219 | f.debug_tuple("Properties") 220 | .field(&self.pairs.as_slice()) 221 | .finish() 222 | } 223 | } 224 | 225 | impl<'a> IntoIterator for &'a Properties { 226 | type Item = as Iterator>::Item; 227 | 228 | type IntoIter = Iter<'a>; 229 | 230 | fn into_iter(self) -> Self::IntoIter { 231 | self.iter() 232 | } 233 | } 234 | 235 | impl<'a> IntoIterator for &'a mut Properties { 236 | type Item = as Iterator>::Item; 237 | 238 | type IntoIter = IterMut<'a>; 239 | 240 | fn into_iter(self) -> Self::IntoIter { 241 | self.iter_mut() 242 | } 243 | } 244 | 245 | impl, V: Into> FromIterator<(K, V)> for Properties { 246 | fn from_iter>(iter: T) -> Self { 247 | let mut result = Properties::new(); 248 | result.extend(iter); 249 | result 250 | } 251 | } 252 | 253 | impl, V: Into> Extend<(K, V)> for Properties { 254 | fn extend>(&mut self, iter: T) { 255 | let iter = iter.into_iter(); 256 | let min_len = iter.size_hint().0; 257 | self.pairs.reserve(min_len); 258 | self.idxes.reserve(min_len); 259 | for (k, v) in iter { 260 | let k = k.as_ref(); 261 | let v = v.into(); 262 | self.insert_raw_for_key(k, v); 263 | } 264 | } 265 | } 266 | 267 | /// Trait for types that can add properties to a [`Properties`] map. 268 | pub trait PropertiesSource { 269 | /// Adds properties that apply to a file at the specified path 270 | /// to the provided [`Properties`]. 271 | fn apply_to( 272 | self, 273 | props: &mut Properties, 274 | path: impl AsRef, 275 | ) -> Result<(), crate::Error>; 276 | } 277 | 278 | impl<'a> PropertiesSource for &'a Properties { 279 | fn apply_to( 280 | self, 281 | props: &mut Properties, 282 | _: impl AsRef, 283 | ) -> Result<(), crate::Error> { 284 | props.extend(self.pairs.iter().cloned()); 285 | Ok(()) 286 | } 287 | } 288 | -------------------------------------------------------------------------------- /src/properties/iter.rs: -------------------------------------------------------------------------------- 1 | #![allow(unused_imports)] 2 | use crate::properties::{Properties, RawValue}; 3 | 4 | macro_rules! impls { 5 | ($name:ident, $valuetype:ty) => { 6 | impl<'a> Iterator for $name<'a> { 7 | type Item = (&'a str, $valuetype); 8 | #[cfg(not(feature = "allow-empty-values"))] 9 | fn next(&mut self) -> Option { 10 | loop { 11 | let pair = self.0.next()?; 12 | if pair.1.is_unset() { 13 | continue; 14 | } 15 | let (ref key, val) = pair; 16 | break Some((key, val)); 17 | } 18 | } 19 | #[cfg(feature = "allow-empty-values")] 20 | fn next(&mut self) -> Option { 21 | let pair = self.0.next()?; 22 | let (ref key, val) = pair; 23 | Some((key, val)) 24 | } 25 | 26 | #[cfg(not(feature = "allow-empty-values"))] 27 | fn size_hint(&self) -> (usize, Option) { 28 | (0, self.0.size_hint().1) 29 | } 30 | #[cfg(feature = "allow-empty-values")] 31 | fn size_hint(&self) -> (usize, Option) { 32 | self.0.size_hint() 33 | } 34 | } 35 | impl<'a> DoubleEndedIterator for $name<'a> { 36 | #[cfg(not(feature = "allow-empty-values"))] 37 | fn next_back(&mut self) -> Option { 38 | loop { 39 | let pair = self.0.next_back()?; 40 | if pair.1.is_unset() { 41 | continue; 42 | } 43 | let (ref key, val) = pair; 44 | break Some((key, val)); 45 | } 46 | } 47 | #[cfg(feature = "allow-empty-values")] 48 | fn next_back(&mut self) -> Option { 49 | let pair = self.0.next_back()?; 50 | let (ref key, val) = pair; 51 | Some((key, val)) 52 | } 53 | } 54 | impl<'a> std::iter::FusedIterator for $name<'a> {} 55 | //TODO: PartialEq/Eq? 56 | }; 57 | } 58 | 59 | /// An iterator over [`Properties`]. 60 | #[derive(Clone)] 61 | pub struct Iter<'a>(pub(super) std::slice::Iter<'a, (String, RawValue)>); 62 | 63 | impls! {Iter, &'a RawValue} 64 | 65 | /// An iterator over [`Properties`] that allows value mutation. 66 | pub struct IterMut<'a>(pub(super) std::slice::IterMut<'a, (String, RawValue)>); 67 | 68 | impls! {IterMut, &'a mut RawValue} 69 | -------------------------------------------------------------------------------- /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 | 6 | use super::{PropertyKey, PropertyValue}; 7 | use crate::rawvalue::RawValue; 8 | 9 | use std::fmt::Display; 10 | 11 | /// Error for common property parse failures. 12 | #[derive(Clone, Copy, Debug)] 13 | pub struct UnknownValueError; 14 | 15 | impl Display for UnknownValueError { 16 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 17 | write!(f, "unknown value") 18 | } 19 | } 20 | 21 | impl std::error::Error for UnknownValueError {} 22 | 23 | //TODO: Deduplicate these macros a bit? 24 | 25 | macro_rules! property_choice { 26 | ($prop_id:ident, $name:literal; $(($variant:ident, $string:literal)),+) => { 27 | // MISTAKE: These need to be #[non_exhaustive], 28 | // but adding it would be a breaking change. 29 | // Hold off on adding it until the breakage would need to happen anyway. 30 | #[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)] 31 | #[repr(u8)] 32 | #[doc = concat!("The [`",$name,"`](https://github.com/editorconfig/editorconfig/wiki/EditorConfig-Properties#",$name,") property.")] 33 | #[allow(missing_docs)] 34 | pub enum $prop_id {$($variant),+} 35 | 36 | impl PropertyValue for $prop_id { 37 | const MAYBE_UNSET: bool = false; 38 | type Err = UnknownValueError; 39 | fn parse(raw: &RawValue) -> Result { 40 | match raw.into_str().to_lowercase().as_str() { 41 | $($string => Ok($prop_id::$variant),)+ 42 | _ => Err(UnknownValueError) 43 | } 44 | } 45 | } 46 | 47 | impl From<$prop_id> for RawValue { 48 | fn from(val: $prop_id) -> RawValue { 49 | match val { 50 | $($prop_id::$variant => RawValue::from($string)),* 51 | } 52 | } 53 | } 54 | 55 | impl PropertyKey for $prop_id { 56 | fn key() -> &'static str {$name} 57 | } 58 | 59 | impl Display for $prop_id { 60 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 61 | write!(f, "{}", match self { 62 | $($prop_id::$variant => $string),* 63 | }) 64 | } 65 | } 66 | } 67 | } 68 | 69 | macro_rules! property_valued { 70 | ( 71 | $prop_id:ident, $name:literal, $value_type:ty; 72 | $(($variant:ident, $string:literal)),* 73 | ) => { 74 | #[derive(Clone, Copy, PartialEq, Eq, Debug)] 75 | #[doc = concat!("The [`",$name,"`](https://github.com/editorconfig/editorconfig/wiki/EditorConfig-Properties#",$name,") property.")] 76 | #[allow(missing_docs)] 77 | pub enum $prop_id { 78 | Value($value_type) 79 | $(,$variant)* 80 | } 81 | 82 | impl PropertyValue for $prop_id { 83 | const MAYBE_UNSET: bool = false; 84 | type Err = UnknownValueError; 85 | fn parse(raw: &RawValue) -> Result { 86 | match raw.into_str().to_lowercase().as_str() { 87 | $($string => Ok($prop_id::$variant),)* 88 | v => v.parse::<$value_type>().map(Self::Value).or(Err(UnknownValueError)) 89 | } 90 | } 91 | } 92 | 93 | impl From<$prop_id> for RawValue { 94 | fn from(val: $prop_id) -> RawValue { 95 | match val { 96 | $prop_id::Value(v) => RawValue::from(v.to_string()), 97 | $($prop_id::$variant => RawValue::from($string)),* 98 | } 99 | } 100 | } 101 | 102 | impl PropertyKey for $prop_id { 103 | fn key() -> &'static str {$name} 104 | } 105 | 106 | impl Display for $prop_id { 107 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 108 | match self { 109 | $prop_id::Value(v) => write!(f, "{}", v), 110 | $($prop_id::$variant => write!(f, "{}", $string)),* 111 | } 112 | } 113 | } 114 | } 115 | } 116 | 117 | property_choice! { 118 | IndentStyle, "indent_style"; 119 | (Tabs, "tab"), 120 | (Spaces, "space") 121 | } 122 | 123 | // NOTE: 124 | // The spec and the wiki disagree on the valid range of indent/tab sizes. 125 | // The spec says "whole numbers" for both, 126 | // whereas the wiki says "an integer"/"a positive integer" respectively. 127 | // This implementation follows the spec strictly here. 128 | // Notably, it will happily consider sizes of 0 valid. 129 | 130 | property_valued! {IndentSize, "indent_size", usize; (UseTabWidth, "tab")} 131 | property_valued! {TabWidth, "tab_width", usize;} 132 | 133 | property_choice! { 134 | EndOfLine, "end_of_line"; 135 | (Lf, "lf"), 136 | (CrLf, "crlf"), 137 | (Cr, "cr") 138 | } 139 | 140 | property_choice! { 141 | Charset, "charset"; 142 | (Utf8, "utf-8"), 143 | (Latin1, "latin1"), 144 | (Utf16Le, "utf-16le"), 145 | (Utf16Be, "utf-16be"), 146 | (Utf8Bom, "utf-8-bom") 147 | } 148 | 149 | property_valued! {TrimTrailingWs, "trim_trailing_whitespace", bool;} 150 | property_valued! {FinalNewline, "insert_final_newline", bool;} 151 | property_valued! {MaxLineLen, "max_line_length", usize; (Off, "off")} 152 | 153 | // As of the authorship of this comment, spelling_language isn't on the wiki. 154 | // Ooop. 155 | 156 | #[cfg(feature = "language-tags")] 157 | /// The `spelling_language` property added by EditorConfig 0.16. 158 | #[derive(Clone, PartialEq, Eq, Hash, Debug)] 159 | #[allow(missing_docs)] 160 | pub enum SpellingLanguage { 161 | Value(crate::language_tags::LanguageTag), 162 | } 163 | 164 | #[cfg(feature = "language-tags")] 165 | impl PropertyValue for SpellingLanguage { 166 | const MAYBE_UNSET: bool = false; 167 | type Err = crate::language_tags::ParseError; 168 | fn parse(raw: &RawValue) -> Result { 169 | if let Some(string) = raw.into_option() { 170 | string.parse().map(SpellingLanguage::Value) 171 | } else { 172 | Err(crate::language_tags::ParseError::EmptySubtag) 173 | } 174 | } 175 | } 176 | 177 | #[cfg(feature = "language-tags")] 178 | impl From for RawValue { 179 | fn from(val: SpellingLanguage) -> RawValue { 180 | match val { 181 | SpellingLanguage::Value(v) => v.to_string().into(), 182 | } 183 | } 184 | } 185 | 186 | #[cfg(feature = "language-tags")] 187 | impl PropertyKey for SpellingLanguage { 188 | fn key() -> &'static str { 189 | "spelling_language" 190 | } 191 | } 192 | 193 | #[cfg(feature = "language-tags")] 194 | impl Display for SpellingLanguage { 195 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 196 | match self { 197 | SpellingLanguage::Value(v) => v.fmt(f), 198 | } 199 | } 200 | } 201 | 202 | /// All the keys of the standard properties. 203 | /// 204 | /// Can be used to determine if a property is defined in the specification or not. 205 | pub static STANDARD_KEYS: &[&str] = &[ 206 | "indent_size", 207 | "indent_style", 208 | "tab_width", 209 | "end_of_line", 210 | "charset", 211 | "trim_trailing_whitespace", 212 | "insert_final_newline", 213 | "spelling_language", 214 | // NOT "max_line_length". 215 | ]; 216 | -------------------------------------------------------------------------------- /src/rawvalue.rs: -------------------------------------------------------------------------------- 1 | //! Types and utilities related to unparsed EditorConfig values. 2 | 3 | use crate::PropertyValue; 4 | 5 | use std::borrow::Cow; 6 | 7 | /// An unset `RawValue`. 8 | /// 9 | /// Not all unset `&RawValues` returned by this library are referentially equal to this one. 10 | /// This exists to provide an unset raw value for whenever a reference to one is necessary. 11 | pub static UNSET: RawValue = RawValue { 12 | value: Cow::Borrowed(""), 13 | #[cfg(feature = "track-source")] 14 | source: None, 15 | }; 16 | 17 | /// An unparsed property value. 18 | /// 19 | /// This is conceptually an optional non-empty string with some convenience methods. 20 | /// With the `track-source` feature, 21 | /// objects of this type can also track the file and line number they originate from. 22 | #[derive(Clone, Debug, Default)] 23 | pub struct RawValue { 24 | value: Cow<'static, str>, 25 | #[cfg(feature = "track-source")] 26 | source: Option<(std::sync::Arc, usize)>, 27 | } 28 | 29 | // Manual-impl (Partial)Eq, (Partial)Ord, and Hash so that the source isn't considered. 30 | 31 | impl PartialEq for RawValue { 32 | fn eq(&self, other: &Self) -> bool { 33 | self.value == other.value 34 | } 35 | } 36 | impl Eq for RawValue {} 37 | impl PartialOrd for RawValue { 38 | fn partial_cmp(&self, other: &Self) -> Option { 39 | Some(self.value.cmp(&other.value)) 40 | } 41 | } 42 | impl Ord for RawValue { 43 | fn cmp(&self, other: &Self) -> std::cmp::Ordering { 44 | self.value.cmp(&other.value) 45 | } 46 | } 47 | impl std::hash::Hash for RawValue { 48 | fn hash(&self, state: &mut H) { 49 | state.write(self.value.as_bytes()); 50 | state.write_u8(0); 51 | } 52 | } 53 | 54 | impl RawValue { 55 | #[must_use] 56 | fn detect_unset(&self) -> Option { 57 | if self.is_unset() { 58 | Some(false) 59 | } else if "unset".eq_ignore_ascii_case(self.value.as_ref()) { 60 | Some(true) 61 | } else { 62 | None 63 | } 64 | } 65 | 66 | #[cfg(feature = "track-source")] 67 | /// Returns the path to the file and the line number that this value originates from. 68 | /// 69 | /// The line number is 1-indexed to match convention; 70 | /// the first line will have a line number of 1 rather than 0. 71 | pub fn source(&self) -> Option<(&std::path::Path, usize)> { 72 | self.source 73 | .as_ref() 74 | .map(|(path, line)| (std::sync::Arc::as_ref(path), *line)) 75 | } 76 | 77 | #[cfg(feature = "track-source")] 78 | /// Sets the path and line number from which this value originated. 79 | /// 80 | /// The line number should be 1-indexed to match convention; 81 | /// the first line should have a line number of 1 rather than 0. 82 | pub fn set_source(&mut self, path: impl Into>, line: usize) { 83 | self.source = Some((path.into(), line)) 84 | } 85 | 86 | #[cfg(feature = "track-source")] 87 | /// Clears the path and line number from which this value originated. 88 | pub fn clear_source(&mut self) { 89 | self.source = None; 90 | } 91 | 92 | /// Returns true if the value is unset. 93 | /// 94 | /// Does not handle values of "unset". 95 | /// See [`RawValue::filter_unset`]. 96 | pub fn is_unset(&self) -> bool { 97 | self.value.is_empty() 98 | } 99 | 100 | /// Returns a reference to.an unset `RawValue` 101 | /// if the value case-insensitively matches `"unset"`, 102 | /// otherwise returns `self`. 103 | #[must_use] 104 | pub fn filter_unset(&self) -> &Self { 105 | if let Some(true) = self.detect_unset() { 106 | &UNSET 107 | } else { 108 | self 109 | } 110 | } 111 | 112 | /// Changes `self` to unset 113 | /// if the value case-insensitively matches `"unset"`. 114 | pub fn filter_unset_mut(&mut self) -> &mut Self { 115 | if let Some(true) = self.detect_unset() { 116 | *self = UNSET.clone(); 117 | } 118 | self 119 | } 120 | 121 | /// Converts this `RawValue` into a [`Result`]. 122 | /// 123 | /// This function filters out values of "unset". 124 | /// The `bool` in the `Err` variant will be false 125 | /// if and only if the value was not set. 126 | pub fn into_result(&self) -> Result<&str, bool> { 127 | if let Some(v) = self.detect_unset() { 128 | Err(v) 129 | } else { 130 | Ok(self.value.as_ref()) 131 | } 132 | } 133 | 134 | /// Converts this `RawValue` into an [`Option`]. 135 | pub fn into_option(&self) -> Option<&str> { 136 | Some(self.value.as_ref()).filter(|v| !v.is_empty()) 137 | } 138 | 139 | /// Converts this `RawValue` into `&str`. 140 | /// 141 | /// If the value was not set, returns "unset". 142 | pub fn into_str(&self) -> &str { 143 | if self.is_unset() { 144 | "unset" 145 | } else { 146 | self.value.as_ref() 147 | } 148 | } 149 | 150 | /// Sets the contained string value. 151 | pub fn set>(&mut self, val: T) -> &mut Self { 152 | *self = val.into(); 153 | self 154 | } 155 | 156 | /// Attempts to parse the contained value. 157 | /// 158 | /// If the value is unset, returns `Err(None)`. 159 | pub fn parse(&self) -> Result> { 160 | let this = if T::MAYBE_UNSET { 161 | self.filter_unset() 162 | } else { 163 | self 164 | }; 165 | if this.is_unset() { 166 | Err(None) 167 | } else { 168 | T::parse(this).map_err(Some) 169 | } 170 | } 171 | 172 | /// Returns a lowercased version of `self`. 173 | #[must_use] 174 | pub fn to_lowercase(&self) -> Self { 175 | Self { 176 | value: Cow::Owned(self.value.to_lowercase()), 177 | #[cfg(feature = "track-source")] 178 | source: self.source.clone(), 179 | } 180 | } 181 | } 182 | 183 | impl std::fmt::Display for RawValue { 184 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 185 | write!(f, "{}", self.value.as_ref()) 186 | } 187 | } 188 | 189 | impl From for RawValue { 190 | fn from(value: String) -> Self { 191 | RawValue { 192 | value: Cow::Owned(value), 193 | #[cfg(feature = "track-source")] 194 | source: None, 195 | } 196 | } 197 | } 198 | 199 | impl From<&'static str> for RawValue { 200 | fn from(value: &'static str) -> Self { 201 | if value.is_empty() { 202 | UNSET.clone() 203 | } else { 204 | RawValue { 205 | value: Cow::Borrowed(value), 206 | #[cfg(feature = "track-source")] 207 | source: None, 208 | } 209 | } 210 | } 211 | } 212 | -------------------------------------------------------------------------------- /src/section.rs: -------------------------------------------------------------------------------- 1 | use crate::glob::Glob; 2 | use crate::{rawvalue::RawValue, Properties}; 3 | 4 | use std::path::Path; 5 | 6 | // Glob internals aren't stable enough to safely implement PartialEq here. 7 | 8 | /// One section of an EditorConfig file. 9 | #[derive(Clone)] 10 | pub struct Section { 11 | pattern: Glob, 12 | props: crate::Properties, 13 | } 14 | 15 | impl Section { 16 | /// Constrcts a new [`Section`] that applies to files matching the specified pattern. 17 | pub fn new(pattern: &str) -> Section { 18 | Section { 19 | pattern: Glob::new(pattern), 20 | props: crate::Properties::new(), 21 | } 22 | } 23 | /// Returns a shared reference to the internal [`Properties`] map. 24 | pub fn props(&self) -> &Properties { 25 | &self.props 26 | } 27 | /// Returns a mutable reference to the internal [`Properties`] map. 28 | pub fn props_mut(&mut self) -> &mut Properties { 29 | &mut self.props 30 | } 31 | /// Extracts the [`Properties`] map from `self`. 32 | pub fn into_props(self) -> Properties { 33 | self.props 34 | } 35 | /// Adds a property with the specified key, lowercasing the key. 36 | pub fn insert(&mut self, key: impl AsRef, val: impl Into) { 37 | self.props 38 | .insert_raw_for_key(key.as_ref().to_lowercase(), val); 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 | self.pattern.matches(path.as_ref()) 43 | } 44 | } 45 | 46 | impl crate::PropertiesSource for &Section { 47 | /// Adds this section's properties to a [`Properties`]. 48 | /// 49 | /// This implementation is infallible. 50 | fn apply_to( 51 | self, 52 | props: &mut Properties, 53 | path: impl AsRef, 54 | ) -> Result<(), crate::Error> { 55 | let path_ref = path.as_ref(); 56 | if self.applies_to(path_ref) { 57 | let _ = self.props.apply_to(props, path_ref); 58 | } 59 | Ok(()) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/tests.rs: -------------------------------------------------------------------------------- 1 | mod ecparser; 2 | mod glob; 3 | mod linereader; 4 | mod properties; 5 | mod property; 6 | mod version; 7 | -------------------------------------------------------------------------------- /src/tests/ecparser.rs: -------------------------------------------------------------------------------- 1 | fn validate<'a>( 2 | text: &str, 3 | should_be_root: bool, 4 | expected: impl IntoIterator, 5 | ) { 6 | let path = std::sync::Arc::::from(std::path::Path::new(".editorconfig")); 7 | let mut parser = crate::ConfigParser::new_buffered_with_path(text.as_bytes(), Some(path)) 8 | .expect("Should have created the parser"); 9 | assert_eq!(parser.is_root, should_be_root); 10 | for section_expected in expected { 11 | let section = parser.next().unwrap().unwrap(); 12 | let mut iter = section.props().iter(); 13 | #[allow(unused)] 14 | for (key, value, line_no) in section_expected { 15 | let (key_test, value_test) = iter.next().expect("Unexpected end of section"); 16 | assert_eq!(key_test, *key, "unexpected key"); 17 | assert_eq!(value_test.into_str(), *value, "unexpected value"); 18 | #[cfg(feature = "track-source")] 19 | assert_eq!( 20 | value_test.source().map(|(_, idx)| idx), 21 | Some(*line_no), 22 | "unexpected line number" 23 | ) 24 | } 25 | assert!(iter.next().is_none()); 26 | } 27 | assert!(parser.next().is_none()); 28 | } 29 | 30 | macro_rules! expect { 31 | [$([$(($key:literal, $value:literal, $line_no:literal)),*]),*] => { 32 | [$(&[$(($key, $value, $line_no)),*][..]),*] 33 | } 34 | } 35 | 36 | #[test] 37 | fn empty() { 38 | validate("", false, expect![]); 39 | } 40 | 41 | #[test] 42 | fn prelude() { 43 | validate("root = true\nroot = false", false, expect![]); 44 | validate("root = true", true, expect![]); 45 | validate("Root = True", true, expect![]); 46 | validate("# hello world", false, expect![]); 47 | } 48 | 49 | #[test] 50 | fn prelude_unknown() { 51 | validate("foo = bar", false, expect![]); 52 | validate("foo = bar\nroot = true", true, expect![]); 53 | } 54 | 55 | #[test] 56 | fn sections_empty() { 57 | validate("[foo]", false, expect![[]]); 58 | validate("[foo]\n[bar]", false, expect![[], []]); 59 | } 60 | 61 | #[test] 62 | fn sections() { 63 | validate( 64 | "[foo]\nbk=bv\nak=av", 65 | false, 66 | expect![[("bk", "bv", 2), ("ak", "av", 3)]], 67 | ); 68 | validate( 69 | "[foo]\nbk=bv\n[bar]\nak=av", 70 | false, 71 | expect![[("bk", "bv", 2)], [("ak", "av", 4)]], 72 | ); 73 | validate( 74 | "[foo]\nk=a\n[bar]\nk=b", 75 | false, 76 | expect![[("k", "a", 2)], [("k", "b", 4)]], 77 | ); 78 | } 79 | 80 | #[test] 81 | fn trailing_newline() { 82 | validate("[foo]\nbar=baz\n", false, expect![[("bar", "baz", 2)]]); 83 | validate("[foo]\nbar=baz\n\n", false, expect![[("bar", "baz", 2)]]); 84 | } 85 | 86 | #[test] 87 | fn section_with_comment_after_it() { 88 | validate( 89 | "[/*] # ignore this comment\nk=v", 90 | false, 91 | expect![[("k", "v", 2)]], 92 | ); 93 | } 94 | 95 | #[test] 96 | fn duplicate_key() { 97 | validate("[*]\nfoo=bar\nfoo=baz", false, expect![[("foo", "baz", 3)]]); 98 | } 99 | -------------------------------------------------------------------------------- /src/tests/glob.rs: -------------------------------------------------------------------------------- 1 | pub fn test<'a, 'b>( 2 | pattern: &str, 3 | valid: impl IntoIterator, 4 | invalid: impl IntoIterator, 5 | ) { 6 | use crate::glob::Glob; 7 | let glob = Glob::new(pattern); 8 | for path in valid { 9 | assert!( 10 | glob.matches(path.as_ref()), 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.as_ref()), 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 | -------------------------------------------------------------------------------- /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 | #[cfg(feature = "allow-empty-values")] 23 | ("foo =", Pair("foo", "")), 24 | #[cfg(feature = "allow-empty-values")] 25 | ("foo = ", Pair("foo", "")), 26 | ]) 27 | } 28 | 29 | #[test] 30 | fn valid_sections() { 31 | use Line::Section; 32 | test_lines(&[ 33 | ("[foo]", Section("foo")), 34 | ("[[foo]]", Section("[foo]")), 35 | ("[ foo ]", Section(" foo ")), 36 | ("[][]]", Section("][]")), 37 | ("[Foo]", Section("Foo")), 38 | (" [foo] ", Section("foo")), 39 | ("[a=b]", Section("a=b")), 40 | ("[#foo]", Section("#foo")), 41 | ("[foo] #comment", Section("foo")), 42 | ("[foo] ;comment", Section("foo")), 43 | ]) 44 | } 45 | 46 | #[test] 47 | fn valid_nothing() { 48 | use Line::Nothing; 49 | test_lines(&[ 50 | ("\t", Nothing), 51 | ("\r", Nothing), 52 | ("", Nothing), 53 | (" ", Nothing), 54 | (";comment", Nothing), 55 | ("#comment", Nothing), 56 | (" # comment", Nothing), 57 | ("# [section]", Nothing), 58 | ("# foo=bar", Nothing), 59 | ]) 60 | } 61 | 62 | #[test] 63 | fn invalid() { 64 | let lines = [ 65 | "[]", 66 | "[close", 67 | "open]", 68 | "][", 69 | "nonproperty", 70 | "=", 71 | " = nokey", 72 | #[cfg(not(feature = "allow-empty-values"))] 73 | "noval = ", 74 | ]; 75 | for line in lines { 76 | assert!(matches!( 77 | parse_line(line).unwrap_err(), 78 | ParseError::InvalidLine 79 | )) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/tests/properties.rs: -------------------------------------------------------------------------------- 1 | use crate::{rawvalue::RawValue, 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!(props.get_raw_for_key(s).into_option(), Some(s)) 18 | } 19 | // Ensure that they keys are returned in order. 20 | assert!(props.iter().map(|k| k.0).eq(BASIC_KEYS.iter().cloned())) 21 | } 22 | 23 | #[test] 24 | fn from_iter() { 25 | let props: Properties = zip_self().collect(); 26 | test_basic_keys(&props); 27 | } 28 | 29 | #[test] 30 | fn insert() { 31 | let mut props = Properties::new(); 32 | for s in BASIC_KEYS { 33 | props.insert_raw_for_key(s, s); 34 | } 35 | test_basic_keys(&props); 36 | } 37 | 38 | #[test] 39 | fn insert_replacing() { 40 | let mut props: Properties = zip_alts().collect(); 41 | for (k, v) in zip_alts() { 42 | let old = props 43 | .get_raw_for_key(k) 44 | .into_option() 45 | .expect("missing pair") 46 | .to_owned(); 47 | assert_eq!(old, v); 48 | props.insert_raw_for_key(k, k); 49 | } 50 | test_basic_keys(&props); 51 | } 52 | 53 | #[test] 54 | fn try_insert() { 55 | let mut props = Properties::new(); 56 | for s in BASIC_KEYS { 57 | assert!(props.try_insert_raw_for_key(s, s).is_ok()); 58 | } 59 | test_basic_keys(&props); 60 | } 61 | 62 | #[test] 63 | fn try_insert_replacing() { 64 | let mut props: Properties = zip_self().collect(); 65 | for (k, v) in zip_alts() { 66 | assert_eq!( 67 | props 68 | .try_insert_raw_for_key(k, k) 69 | .expect_err("try_insert wrongly returns Ok for same value") 70 | .into_str(), 71 | k 72 | ); 73 | assert_eq!( 74 | props 75 | .try_insert_raw_for_key(k, v) 76 | .expect_err("try_insert wrongly returns Ok for update") 77 | .into_str(), 78 | k 79 | ); 80 | } 81 | } 82 | 83 | #[test] 84 | fn apply_empty_to() { 85 | let mut props = Properties::new(); 86 | props.insert_raw_for_key("foo", "a"); 87 | props.insert_raw_for_key("bar", "b"); 88 | let mut empty_pairs = Properties::new(); 89 | empty_pairs.insert_raw_for_key("bar", ""); 90 | empty_pairs.insert_raw_for_key("baz", ""); 91 | assert_eq!(empty_pairs.len(), 2); 92 | empty_pairs 93 | .apply_to(&mut props, "") 94 | .expect("Properties::apply_to should be infallible"); 95 | assert_eq!(props.len(), 3); 96 | assert_eq!(props.get_raw_for_key("bar"), &RawValue::from("")); 97 | } 98 | -------------------------------------------------------------------------------- /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 | #[cfg(feature = "language-tags")] 23 | contained!(SpellingLanguage); 24 | assert!(!STANDARD_KEYS.contains(&MaxLineLen::key())); // Not MaxLineLen 25 | } 26 | 27 | #[cfg(feature = "language-tags")] 28 | #[test] 29 | fn spelling_language() { 30 | use crate::property::SpellingLanguage; 31 | use crate::rawvalue::RawValue; 32 | use crate::PropertyValue; 33 | // This is more testing language-tags than anything, 34 | // but for language-tags to be useful here, 35 | let testcase_en = RawValue::from("en"); 36 | let parsed = match SpellingLanguage::parse(&testcase_en) { 37 | Ok(SpellingLanguage::Value(v)) => v, 38 | e => { 39 | let v = e.expect("parsing should succeed"); 40 | panic!("unexpected value {v:?}"); 41 | } 42 | }; 43 | assert_eq!(parsed.primary_language(), "en"); 44 | let testcase_en_us = RawValue::from("en-US"); 45 | let parsed = match SpellingLanguage::parse(&testcase_en_us) { 46 | Ok(SpellingLanguage::Value(v)) => v, 47 | e => { 48 | let v = e.expect("parsing should succeed"); 49 | panic!("unexpected value {v:?}"); 50 | } 51 | }; 52 | assert_eq!(parsed.primary_language(), "en"); 53 | assert_eq!(parsed.region(), Some("US")); 54 | } 55 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/traits.rs: -------------------------------------------------------------------------------- 1 | use crate::rawvalue::RawValue; 2 | 3 | /// Trait for types that be parsed out of [`RawValue`]s. 4 | /// 5 | /// Types that implement this trait should also implement `Into`. 6 | pub trait PropertyValue: Sized { 7 | /// Indicates whether a value that is case-insensitively equal to "unset" 8 | /// should NOT be treated as if the value is unset. 9 | /// 10 | /// This will typically be false for non-string properties. 11 | const MAYBE_UNSET: bool; 12 | 13 | /// The type of value returned on a failed parse. 14 | type Err; 15 | 16 | /// Parses a value from a not-unset [`RawValue`]. 17 | /// 18 | /// This usually shouldn't be called directly. 19 | /// See [`crate::Properties`] or [`RawValue::parse`]. 20 | fn parse(value: &RawValue) -> Result; 21 | } 22 | 23 | /// Trait for types that are associated with property names. 24 | /// 25 | /// Types that implement this trait will usually also implement [`PropertyValue`]. 26 | pub trait PropertyKey { 27 | /// The lowercase string key for this property. 28 | /// 29 | /// Used to look up the value in a [`crate::Properties`] map. 30 | fn key() -> &'static str; 31 | } 32 | 33 | /// Tests if the result of parsing the result of an `Into` conversion 34 | /// is *not unequal* to the original value. 35 | #[cfg(test)] 36 | pub fn test_reparse(initial: T) 37 | where 38 | T: Clone + PropertyValue + Into + std::fmt::Debug + PartialEq, 39 | { 40 | let written: RawValue = initial.clone().into(); 41 | let result = T::parse(&written).expect("reparse errored"); 42 | assert!( 43 | !result.ne(&initial), 44 | "reparsed value is unequal to original; expected `{:?}`, got `{:?}`", 45 | initial, 46 | result 47 | ) 48 | } 49 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 = ["allow-empty-values", "track-source"] } 10 | semver = "1.0" 11 | clap = { version = "3.1", features = ["derive"] } 12 | 13 | [[bin]] 14 | name = "ec4rs-parse" 15 | -------------------------------------------------------------------------------- /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.to_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 | --------------------------------------------------------------------------------