├── .gitignore ├── Cargo.toml ├── .editorconfig ├── .github └── workflows │ └── nix-build.yml ├── flake.nix ├── default.nix ├── LICENSE ├── flake.lock ├── README.md ├── Cargo.lock └── src └── main.rs /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "nixf-diagnose" 3 | version = "0.1.0" 4 | edition = "2018" 5 | 6 | [dependencies] 7 | ariadne = "0.1" 8 | serde_json = "1.0" 9 | clap = { version = "4.0", features = ["derive"] } 10 | which = "4.2" 11 | rayon = "*" 12 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_style = space 7 | insert_final_newline = true 8 | trim_trailing_whitespace = true 9 | 10 | [*.nix,*.yml] 11 | indent_size = 2 12 | 13 | [*.rs] 14 | indent_size = 4 15 | -------------------------------------------------------------------------------- /.github/workflows/nix-build.yml: -------------------------------------------------------------------------------- 1 | name: Nix build 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - main 8 | 9 | jobs: 10 | flake-default: 11 | strategy: 12 | matrix: 13 | os: [ ubuntu-latest, macos-latest ] 14 | runs-on: ${{ matrix.os }} 15 | steps: 16 | - uses: cachix/install-nix-action@v31 17 | with: 18 | github_access_token: ${{ secrets.GITHUB_TOKEN }} 19 | - uses: actions/checkout@v4 20 | - name: Build flake 21 | run: nix build -L #. 22 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | inputs = { 3 | nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; 4 | 5 | flake-parts.url = "github:hercules-ci/flake-parts"; 6 | }; 7 | outputs = 8 | { 9 | flake-parts, 10 | nixpkgs, 11 | ... 12 | }@inputs: 13 | flake-parts.lib.mkFlake { inherit inputs; } { 14 | 15 | perSystem = 16 | { pkgs, ... }: 17 | { 18 | packages.default = pkgs.callPackage ./default.nix { }; 19 | }; 20 | 21 | systems = nixpkgs.lib.systems.flakeExposed; 22 | }; 23 | } 24 | -------------------------------------------------------------------------------- /default.nix: -------------------------------------------------------------------------------- 1 | { 2 | lib, 3 | rustPlatform, 4 | nixf, 5 | }: 6 | 7 | rustPlatform.buildRustPackage (finalAttrs: { 8 | pname = "nixf-diagnose"; 9 | version = "nightly"; 10 | 11 | src = ./.; 12 | 13 | env.NIXF_TIDY_PATH = lib.getExe nixf; 14 | 15 | useFetchCargoVendor = true; 16 | cargoHash = "sha256-LutCktLHpfl5aMvN9RW0IL9nojcq4j2kjc9zfeePCMg="; 17 | 18 | meta = { 19 | description = "CLI wrapper for nixf-tidy with fancy diagnostic output"; 20 | mainProgram = "nixf-diagnose"; 21 | homepage = "https://github.com/inclyc/nixf-diagnose"; 22 | license = lib.licenses.mit; 23 | maintainers = with lib.maintainers; [ inclyc ]; 24 | }; 25 | }) 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) [year] [fullname] 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "flake-parts": { 4 | "inputs": { 5 | "nixpkgs-lib": "nixpkgs-lib" 6 | }, 7 | "locked": { 8 | "lastModified": 1743550720, 9 | "narHash": "sha256-hIshGgKZCgWh6AYJpJmRgFdR3WUbkY04o82X05xqQiY=", 10 | "owner": "hercules-ci", 11 | "repo": "flake-parts", 12 | "rev": "c621e8422220273271f52058f618c94e405bb0f5", 13 | "type": "github" 14 | }, 15 | "original": { 16 | "owner": "hercules-ci", 17 | "repo": "flake-parts", 18 | "type": "github" 19 | } 20 | }, 21 | "nixpkgs": { 22 | "locked": { 23 | "lastModified": 1748693115, 24 | "narHash": "sha256-StSrWhklmDuXT93yc3GrTlb0cKSS0agTAxMGjLKAsY8=", 25 | "owner": "NixOS", 26 | "repo": "nixpkgs", 27 | "rev": "910796cabe436259a29a72e8d3f5e180fc6dfacc", 28 | "type": "github" 29 | }, 30 | "original": { 31 | "owner": "NixOS", 32 | "ref": "nixos-unstable", 33 | "repo": "nixpkgs", 34 | "type": "github" 35 | } 36 | }, 37 | "nixpkgs-lib": { 38 | "locked": { 39 | "lastModified": 1743296961, 40 | "narHash": "sha256-b1EdN3cULCqtorQ4QeWgLMrd5ZGOjLSLemfa00heasc=", 41 | "owner": "nix-community", 42 | "repo": "nixpkgs.lib", 43 | "rev": "e4822aea2a6d1cdd36653c134cacfd64c97ff4fa", 44 | "type": "github" 45 | }, 46 | "original": { 47 | "owner": "nix-community", 48 | "repo": "nixpkgs.lib", 49 | "type": "github" 50 | } 51 | }, 52 | "root": { 53 | "inputs": { 54 | "flake-parts": "flake-parts", 55 | "nixpkgs": "nixpkgs" 56 | } 57 | } 58 | }, 59 | "root": "root", 60 | "version": 7 61 | } 62 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # nixf-diagnose 3 | 4 | nixf-diagnose is a CLI wrapper for nixf-tidy with fancy diagnostic output. It provides enhanced error reporting and variable lookup analysis for your source files. 5 | 6 | 7 | ## Usage 8 | 9 | To use nixf-diagnose, you need to provide the path to the input source file. 10 | Optionally, you can specify the path to the nixf-tidy executable and enable or disable variable lookup analysis. 11 | 12 | nixf-diagnose tries to determine the nixf-tidy path in the following order: 13 | 14 | 1. Path provided via `--nixf-tidy-path` CLI argument 15 | 2. Compile-time constant (embedded during build) 16 | 3. Runtime discovery via `which` command 17 | 18 | 19 | ```sh 20 | nixf-diagnose [OPTIONS] [FILES]... 21 | ``` 22 | 23 | Example output: 24 | 25 | ``` 26 | Error: duplicated attrname `a` 27 | ╭─[nixd/test-workspace/redefined.nix:4:5] 28 | │ 29 | 2 │ a = 1; 30 | · ┬ 31 | · ╰── previously declared here 32 | · 33 | 4 │ a = 1; 34 | · ┬ 35 | · ╰── duplicated attrname `a` 36 | ───╯ 37 | ``` 38 | 39 | ### Options 40 | 41 | | Option | Description | 42 | | --------------------------------------- | ------------------------------------------------------------------------------ | 43 | | `--nixf-tidy-path ` | Path to the nixf-tidy executable | 44 | | `--variable-lookup []` | Enable variable lookup analysis [default: true] [possible values: true, false] | 45 | | `-h, --help` | Print help | 46 | | `-V, --version` | Print version | 47 | | `-i, --ignore ` | Ignore diagnostics with specific ids
This can be used multiple times | 48 | | `-o, --only ` | Only run a single diagnostic | 49 | | `--auto-fix` | Automatically apply fixes to source files | 50 | 51 | ## License 52 | 53 | This project is licensed under the MIT License. 54 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "anstream" 7 | version = "0.6.18" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" 10 | dependencies = [ 11 | "anstyle", 12 | "anstyle-parse", 13 | "anstyle-query", 14 | "anstyle-wincon", 15 | "colorchoice", 16 | "is_terminal_polyfill", 17 | "utf8parse", 18 | ] 19 | 20 | [[package]] 21 | name = "anstyle" 22 | version = "1.0.10" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" 25 | 26 | [[package]] 27 | name = "anstyle-parse" 28 | version = "0.2.6" 29 | source = "registry+https://github.com/rust-lang/crates.io-index" 30 | checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" 31 | dependencies = [ 32 | "utf8parse", 33 | ] 34 | 35 | [[package]] 36 | name = "anstyle-query" 37 | version = "1.1.2" 38 | source = "registry+https://github.com/rust-lang/crates.io-index" 39 | checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" 40 | dependencies = [ 41 | "windows-sys", 42 | ] 43 | 44 | [[package]] 45 | name = "anstyle-wincon" 46 | version = "3.0.7" 47 | source = "registry+https://github.com/rust-lang/crates.io-index" 48 | checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" 49 | dependencies = [ 50 | "anstyle", 51 | "once_cell", 52 | "windows-sys", 53 | ] 54 | 55 | [[package]] 56 | name = "ariadne" 57 | version = "0.1.5" 58 | source = "registry+https://github.com/rust-lang/crates.io-index" 59 | checksum = "f1cb2a2046bea8ce5e875551f5772024882de0b540c7f93dfc5d6cf1ca8b030c" 60 | dependencies = [ 61 | "yansi", 62 | ] 63 | 64 | [[package]] 65 | name = "bitflags" 66 | version = "2.6.0" 67 | source = "registry+https://github.com/rust-lang/crates.io-index" 68 | checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" 69 | 70 | [[package]] 71 | name = "clap" 72 | version = "4.5.38" 73 | source = "registry+https://github.com/rust-lang/crates.io-index" 74 | checksum = "ed93b9805f8ba930df42c2590f05453d5ec36cbb85d018868a5b24d31f6ac000" 75 | dependencies = [ 76 | "clap_builder", 77 | "clap_derive", 78 | ] 79 | 80 | [[package]] 81 | name = "clap_builder" 82 | version = "4.5.38" 83 | source = "registry+https://github.com/rust-lang/crates.io-index" 84 | checksum = "379026ff283facf611b0ea629334361c4211d1b12ee01024eec1591133b04120" 85 | dependencies = [ 86 | "anstream", 87 | "anstyle", 88 | "clap_lex", 89 | "strsim", 90 | ] 91 | 92 | [[package]] 93 | name = "clap_derive" 94 | version = "4.5.32" 95 | source = "registry+https://github.com/rust-lang/crates.io-index" 96 | checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7" 97 | dependencies = [ 98 | "heck", 99 | "proc-macro2", 100 | "quote", 101 | "syn", 102 | ] 103 | 104 | [[package]] 105 | name = "clap_lex" 106 | version = "0.7.4" 107 | source = "registry+https://github.com/rust-lang/crates.io-index" 108 | checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" 109 | 110 | [[package]] 111 | name = "colorchoice" 112 | version = "1.0.3" 113 | source = "registry+https://github.com/rust-lang/crates.io-index" 114 | checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" 115 | 116 | [[package]] 117 | name = "crossbeam-deque" 118 | version = "0.8.6" 119 | source = "registry+https://github.com/rust-lang/crates.io-index" 120 | checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" 121 | dependencies = [ 122 | "crossbeam-epoch", 123 | "crossbeam-utils", 124 | ] 125 | 126 | [[package]] 127 | name = "crossbeam-epoch" 128 | version = "0.9.18" 129 | source = "registry+https://github.com/rust-lang/crates.io-index" 130 | checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" 131 | dependencies = [ 132 | "crossbeam-utils", 133 | ] 134 | 135 | [[package]] 136 | name = "crossbeam-utils" 137 | version = "0.8.21" 138 | source = "registry+https://github.com/rust-lang/crates.io-index" 139 | checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" 140 | 141 | [[package]] 142 | name = "either" 143 | version = "1.13.0" 144 | source = "registry+https://github.com/rust-lang/crates.io-index" 145 | checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" 146 | 147 | [[package]] 148 | name = "errno" 149 | version = "0.3.10" 150 | source = "registry+https://github.com/rust-lang/crates.io-index" 151 | checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" 152 | dependencies = [ 153 | "libc", 154 | "windows-sys", 155 | ] 156 | 157 | [[package]] 158 | name = "heck" 159 | version = "0.5.0" 160 | source = "registry+https://github.com/rust-lang/crates.io-index" 161 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 162 | 163 | [[package]] 164 | name = "home" 165 | version = "0.5.11" 166 | source = "registry+https://github.com/rust-lang/crates.io-index" 167 | checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" 168 | dependencies = [ 169 | "windows-sys", 170 | ] 171 | 172 | [[package]] 173 | name = "is_terminal_polyfill" 174 | version = "1.70.1" 175 | source = "registry+https://github.com/rust-lang/crates.io-index" 176 | checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" 177 | 178 | [[package]] 179 | name = "itoa" 180 | version = "1.0.14" 181 | source = "registry+https://github.com/rust-lang/crates.io-index" 182 | checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" 183 | 184 | [[package]] 185 | name = "libc" 186 | version = "0.2.169" 187 | source = "registry+https://github.com/rust-lang/crates.io-index" 188 | checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" 189 | 190 | [[package]] 191 | name = "linux-raw-sys" 192 | version = "0.4.14" 193 | source = "registry+https://github.com/rust-lang/crates.io-index" 194 | checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" 195 | 196 | [[package]] 197 | name = "memchr" 198 | version = "2.7.4" 199 | source = "registry+https://github.com/rust-lang/crates.io-index" 200 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 201 | 202 | [[package]] 203 | name = "nixf-diagnose" 204 | version = "0.1.0" 205 | dependencies = [ 206 | "ariadne", 207 | "clap", 208 | "rayon", 209 | "serde_json", 210 | "which", 211 | ] 212 | 213 | [[package]] 214 | name = "once_cell" 215 | version = "1.20.2" 216 | source = "registry+https://github.com/rust-lang/crates.io-index" 217 | checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" 218 | 219 | [[package]] 220 | name = "proc-macro2" 221 | version = "1.0.92" 222 | source = "registry+https://github.com/rust-lang/crates.io-index" 223 | checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" 224 | dependencies = [ 225 | "unicode-ident", 226 | ] 227 | 228 | [[package]] 229 | name = "quote" 230 | version = "1.0.38" 231 | source = "registry+https://github.com/rust-lang/crates.io-index" 232 | checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" 233 | dependencies = [ 234 | "proc-macro2", 235 | ] 236 | 237 | [[package]] 238 | name = "rayon" 239 | version = "1.10.0" 240 | source = "registry+https://github.com/rust-lang/crates.io-index" 241 | checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" 242 | dependencies = [ 243 | "either", 244 | "rayon-core", 245 | ] 246 | 247 | [[package]] 248 | name = "rayon-core" 249 | version = "1.12.1" 250 | source = "registry+https://github.com/rust-lang/crates.io-index" 251 | checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" 252 | dependencies = [ 253 | "crossbeam-deque", 254 | "crossbeam-utils", 255 | ] 256 | 257 | [[package]] 258 | name = "rustix" 259 | version = "0.38.42" 260 | source = "registry+https://github.com/rust-lang/crates.io-index" 261 | checksum = "f93dc38ecbab2eb790ff964bb77fa94faf256fd3e73285fd7ba0903b76bedb85" 262 | dependencies = [ 263 | "bitflags", 264 | "errno", 265 | "libc", 266 | "linux-raw-sys", 267 | "windows-sys", 268 | ] 269 | 270 | [[package]] 271 | name = "ryu" 272 | version = "1.0.18" 273 | source = "registry+https://github.com/rust-lang/crates.io-index" 274 | checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" 275 | 276 | [[package]] 277 | name = "serde" 278 | version = "1.0.217" 279 | source = "registry+https://github.com/rust-lang/crates.io-index" 280 | checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70" 281 | dependencies = [ 282 | "serde_derive", 283 | ] 284 | 285 | [[package]] 286 | name = "serde_derive" 287 | version = "1.0.217" 288 | source = "registry+https://github.com/rust-lang/crates.io-index" 289 | checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" 290 | dependencies = [ 291 | "proc-macro2", 292 | "quote", 293 | "syn", 294 | ] 295 | 296 | [[package]] 297 | name = "serde_json" 298 | version = "1.0.134" 299 | source = "registry+https://github.com/rust-lang/crates.io-index" 300 | checksum = "d00f4175c42ee48b15416f6193a959ba3a0d67fc699a0db9ad12df9f83991c7d" 301 | dependencies = [ 302 | "itoa", 303 | "memchr", 304 | "ryu", 305 | "serde", 306 | ] 307 | 308 | [[package]] 309 | name = "strsim" 310 | version = "0.11.1" 311 | source = "registry+https://github.com/rust-lang/crates.io-index" 312 | checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" 313 | 314 | [[package]] 315 | name = "syn" 316 | version = "2.0.93" 317 | source = "registry+https://github.com/rust-lang/crates.io-index" 318 | checksum = "9c786062daee0d6db1132800e623df74274a0a87322d8e183338e01b3d98d058" 319 | dependencies = [ 320 | "proc-macro2", 321 | "quote", 322 | "unicode-ident", 323 | ] 324 | 325 | [[package]] 326 | name = "unicode-ident" 327 | version = "1.0.14" 328 | source = "registry+https://github.com/rust-lang/crates.io-index" 329 | checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" 330 | 331 | [[package]] 332 | name = "utf8parse" 333 | version = "0.2.2" 334 | source = "registry+https://github.com/rust-lang/crates.io-index" 335 | checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" 336 | 337 | [[package]] 338 | name = "which" 339 | version = "4.4.2" 340 | source = "registry+https://github.com/rust-lang/crates.io-index" 341 | checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" 342 | dependencies = [ 343 | "either", 344 | "home", 345 | "once_cell", 346 | "rustix", 347 | ] 348 | 349 | [[package]] 350 | name = "windows-sys" 351 | version = "0.59.0" 352 | source = "registry+https://github.com/rust-lang/crates.io-index" 353 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 354 | dependencies = [ 355 | "windows-targets", 356 | ] 357 | 358 | [[package]] 359 | name = "windows-targets" 360 | version = "0.52.6" 361 | source = "registry+https://github.com/rust-lang/crates.io-index" 362 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 363 | dependencies = [ 364 | "windows_aarch64_gnullvm", 365 | "windows_aarch64_msvc", 366 | "windows_i686_gnu", 367 | "windows_i686_gnullvm", 368 | "windows_i686_msvc", 369 | "windows_x86_64_gnu", 370 | "windows_x86_64_gnullvm", 371 | "windows_x86_64_msvc", 372 | ] 373 | 374 | [[package]] 375 | name = "windows_aarch64_gnullvm" 376 | version = "0.52.6" 377 | source = "registry+https://github.com/rust-lang/crates.io-index" 378 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 379 | 380 | [[package]] 381 | name = "windows_aarch64_msvc" 382 | version = "0.52.6" 383 | source = "registry+https://github.com/rust-lang/crates.io-index" 384 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 385 | 386 | [[package]] 387 | name = "windows_i686_gnu" 388 | version = "0.52.6" 389 | source = "registry+https://github.com/rust-lang/crates.io-index" 390 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 391 | 392 | [[package]] 393 | name = "windows_i686_gnullvm" 394 | version = "0.52.6" 395 | source = "registry+https://github.com/rust-lang/crates.io-index" 396 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 397 | 398 | [[package]] 399 | name = "windows_i686_msvc" 400 | version = "0.52.6" 401 | source = "registry+https://github.com/rust-lang/crates.io-index" 402 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 403 | 404 | [[package]] 405 | name = "windows_x86_64_gnu" 406 | version = "0.52.6" 407 | source = "registry+https://github.com/rust-lang/crates.io-index" 408 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 409 | 410 | [[package]] 411 | name = "windows_x86_64_gnullvm" 412 | version = "0.52.6" 413 | source = "registry+https://github.com/rust-lang/crates.io-index" 414 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 415 | 416 | [[package]] 417 | name = "windows_x86_64_msvc" 418 | version = "0.52.6" 419 | source = "registry+https://github.com/rust-lang/crates.io-index" 420 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 421 | 422 | [[package]] 423 | name = "yansi" 424 | version = "0.5.1" 425 | source = "registry+https://github.com/rust-lang/crates.io-index" 426 | checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec" 427 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use ariadne::{Label, Report, ReportKind, Source}; 2 | use clap::Parser; 3 | use rayon::prelude::*; 4 | use serde_json::Value; 5 | use std::fs::File; 6 | use std::io::{Read, Write}; 7 | use std::process::{Command, Stdio}; 8 | use which::which; 9 | 10 | /// CLI wrapper for nixf-tidy with fancy diagnostic output 11 | #[derive(Parser)] 12 | #[command( 13 | name = "nixf-diagnose", 14 | version = "0.1.0", 15 | author = "Yingchi Long " 16 | )] 17 | struct Args { 18 | /// Path to the nixf-tidy executable 19 | #[arg(long)] 20 | nixf_tidy_path: Option, 21 | 22 | /// Enable variable lookup analysis 23 | #[arg(long, default_value_t = true, default_missing_value="true", num_args=0..=1)] 24 | variable_lookup: bool, 25 | 26 | /// Ignore diagnostics with specific ids 27 | /// 28 | /// This can be used multiple times 29 | #[arg(short, long, value_name = "ID")] 30 | ignore: Vec, 31 | 32 | /// Only run a single diagnostic 33 | #[arg(short, long)] 34 | only: Option, 35 | 36 | /// Automatically apply fixes to source files 37 | #[arg(long)] 38 | auto_fix: bool, 39 | 40 | /// Input source files 41 | files: Vec, 42 | } 43 | 44 | type NixfReport<'a> = (Report<(&'a str, std::ops::Range)>, &'a str, Source); 45 | 46 | #[derive(Debug, Clone)] 47 | struct Edit { 48 | range: std::ops::Range, 49 | new_text: String, 50 | } 51 | 52 | fn apply_fixes_to_content(content: &str, edits: &[Edit]) -> String { 53 | if edits.is_empty() { 54 | return content.to_string(); 55 | } 56 | 57 | // Sort fixes by start position in reverse order to apply from end to beginning. 58 | // This is to avoid the location markers from getting out of sync once the first 59 | // edit is done. 60 | let mut sorted_fixes = edits.to_vec(); 61 | sorted_fixes.sort_by(|a, b| b.range.start.cmp(&a.range.start)); 62 | 63 | let mut result = content.to_string(); 64 | for fix in sorted_fixes { 65 | if fix.range.end <= result.len() { 66 | result.replace_range(fix.range.clone(), &fix.new_text); 67 | } 68 | } 69 | 70 | result 71 | } 72 | 73 | fn build_char_byte_table(s: &str) -> Vec { 74 | let mut table = Vec::new(); 75 | let mut byte_pos = 0; 76 | for c in s.chars() { 77 | table.push(byte_pos); 78 | byte_pos += c.len_utf8(); 79 | } 80 | table 81 | } 82 | 83 | fn byte_to_char_offset(table: &[usize], byte_pos: usize) -> usize { 84 | table.binary_search(&byte_pos).unwrap() 85 | } 86 | 87 | fn process_file<'a>( 88 | variable_lookup: bool, 89 | nixf_tidy_path: &str, 90 | ignore_rules: &[String], 91 | only: &Option, 92 | auto_fix: bool, 93 | input_file: &'a str, 94 | ) -> Vec> { 95 | let mut cmd = Command::new(nixf_tidy_path); 96 | cmd.stdin(Stdio::piped()).stdout(Stdio::piped()); 97 | 98 | if variable_lookup { 99 | cmd.arg("--variable-lookup"); 100 | } 101 | 102 | let mut input = String::new(); 103 | File::open(input_file) 104 | .unwrap_or_else(|e| panic!("Failed to open {}: {}", input_file, e)) 105 | .read_to_string(&mut input) 106 | .unwrap_or_else(|e| panic!("Failed to read {}: {}", input_file, e)); 107 | 108 | let mut child = cmd 109 | .spawn() 110 | .unwrap_or_else(|e| panic!("Failed to execute nixf-tidy: {}", e)); 111 | child 112 | .stdin 113 | .as_mut() 114 | .unwrap() 115 | .write_all(input.as_bytes()) 116 | .unwrap(); 117 | 118 | let char_byte_table = build_char_byte_table(&input); 119 | 120 | let output = child 121 | .wait_with_output() 122 | .unwrap_or_else(|e| panic!("Failed to read output: {}", e)); 123 | 124 | if !output.status.success() { 125 | eprintln!("nixf-tidy failed on file '{input_file}'"); 126 | return vec![]; 127 | } 128 | 129 | let stdout = String::from_utf8(output.stdout).unwrap_or_default(); 130 | let diagnostics: Value = match serde_json::from_str(&stdout) { 131 | Ok(v) => v, 132 | Err(e) => { 133 | eprintln!("Failed to parse JSON from nixf-tidy output: {e}"); 134 | return vec![]; 135 | } 136 | }; 137 | 138 | let mut reports = vec![]; 139 | let mut all_edits = vec![]; 140 | 141 | if let Some(diags) = diagnostics.as_array() { 142 | for diag in diags { 143 | if let ( 144 | Some(sname), 145 | Some(message), 146 | Some(spans), 147 | Some(severity), 148 | Some(args), 149 | Some(notes), 150 | Some(fixes), // Vec, // Fix = { edits, message } 151 | ) = ( 152 | diag.get("sname"), 153 | diag.get("message"), 154 | diag.get("range"), 155 | diag.get("severity"), 156 | diag.get("args"), 157 | diag.get("notes"), 158 | diag.get("fixes"), 159 | ) { 160 | if let Some(rule) = only { 161 | if rule != sname { 162 | continue; // Ignore all except --only 163 | } 164 | } 165 | 166 | if ignore_rules.iter().any(|rule| rule == sname) { 167 | continue; // Ignore this diagnostic 168 | } 169 | 170 | // Collect fixes for auto-fix functionality 171 | // TODO: We currently limit this to one edit per file per run, until 172 | // https://github.com/inclyc/nixf-diagnose/issues/13 173 | // is sorted out. 174 | if auto_fix && all_edits.is_empty() { 175 | if let Some(fixes_array) = fixes.as_array() { 176 | if !fixes_array.is_empty() { 177 | if fixes_array.len() > 1 { 178 | eprintln!( 179 | "Warning: Multiple fixes found for a single diagnostic. Only the first fix will be applied to '{input_file}'." 180 | ); 181 | } 182 | let first_fix = fixes_array.first().unwrap(); 183 | if let Some(edits) = first_fix.get("edits").and_then(|e| e.as_array()) { 184 | for edit in edits { 185 | if let (Some(new_text), Some(range)) = ( 186 | edit.get("newText").and_then(|t| t.as_str()), 187 | edit.get("range"), 188 | ) { 189 | if let (Some(start), Some(end)) = ( 190 | range 191 | .get("lCur") 192 | .and_then(|s| s.get("offset").and_then(|o| o.as_u64())), 193 | range 194 | .get("rCur") 195 | .and_then(|e| e.get("offset").and_then(|o| o.as_u64())), 196 | ) { 197 | all_edits.push(Edit { 198 | range: (start as usize)..(end as usize), 199 | new_text: new_text.to_string(), 200 | }); 201 | } 202 | } 203 | } 204 | continue 205 | } 206 | } 207 | } 208 | } 209 | 210 | let report_kind = match severity.as_i64().unwrap_or(1) { 211 | 0 => ReportKind::Error, 212 | 1 => ReportKind::Error, 213 | 2 => ReportKind::Warning, 214 | 3 => ReportKind::Advice, 215 | 4 => ReportKind::Advice, 216 | _ => ReportKind::Error, 217 | }; 218 | 219 | let mut formatted_message = message.as_str().unwrap_or("Unknown error").to_string(); 220 | if let Some(args_array) = args.as_array() { 221 | for arg in args_array { 222 | if let Some(arg_str) = arg.as_str() { 223 | formatted_message = formatted_message.replacen("{}", arg_str, 1); 224 | } 225 | } 226 | } 227 | 228 | if let (Some(start), Some(end)) = ( 229 | spans 230 | .get("lCur") 231 | .and_then(|s| s.get("offset").and_then(|o| o.as_u64())), 232 | spans 233 | .get("rCur") 234 | .and_then(|e| e.get("offset").and_then(|o| o.as_u64())), 235 | ) { 236 | let start_char = byte_to_char_offset(&char_byte_table, start as usize); 237 | let end_char = byte_to_char_offset(&char_byte_table, end as usize); 238 | let mut report = Report::build(report_kind, input_file, start_char) 239 | .with_message(&formatted_message) 240 | .with_label( 241 | Label::new((input_file, start_char..end_char)) 242 | .with_message(&formatted_message), 243 | ) 244 | .with_code(sname.as_str().unwrap()); 245 | 246 | if let Some(notes_array) = notes.as_array() { 247 | for note in notes_array { 248 | if let (Some(note_message), Some(note_args), Some(note_spans)) = 249 | (note.get("message"), note.get("args"), note.get("range")) 250 | { 251 | let mut formatted_note_message = 252 | note_message.as_str().unwrap_or("Unknown note").to_string(); 253 | if let Some(note_args_array) = note_args.as_array() { 254 | for arg in note_args_array { 255 | if let Some(arg_str) = arg.as_str() { 256 | formatted_note_message = 257 | formatted_note_message.replacen("{}", arg_str, 1); 258 | } 259 | } 260 | } 261 | 262 | if let (Some(note_start), Some(note_end)) = ( 263 | note_spans 264 | .get("lCur") 265 | .and_then(|s| s.get("offset").and_then(|o| o.as_u64())), 266 | note_spans 267 | .get("rCur") 268 | .and_then(|e| e.get("offset").and_then(|o| o.as_u64())), 269 | ) { 270 | let start_char = 271 | byte_to_char_offset(&char_byte_table, note_start as usize); 272 | let end_char = 273 | byte_to_char_offset(&char_byte_table, note_end as usize); 274 | report = report.with_label( 275 | Label::new((input_file, start_char..end_char)) 276 | .with_message(&formatted_note_message), 277 | ); 278 | } 279 | } 280 | } 281 | } 282 | reports.push((report.finish(), input_file, Source::from(&input))); 283 | } 284 | } 285 | } 286 | } 287 | 288 | // Apply edits if auto_fix is enabled 289 | if auto_fix && !all_edits.is_empty() { 290 | let fixed_content = apply_fixes_to_content(&input, &all_edits); 291 | if let Err(e) = std::fs::write(input_file, fixed_content) { 292 | eprintln!("Failed to write fixed content to {input_file}: {e}"); 293 | } else { 294 | eprintln!("Applied {} edits to {}", all_edits.len(), input_file); 295 | } 296 | } 297 | 298 | reports 299 | } 300 | 301 | fn main() { 302 | let args = Args::parse(); 303 | 304 | // Try to determine nixf-tidy path in order: 305 | // 1. Provided CLI argument 306 | // 2. Compile-time constant (from build script) 307 | // 3. Runtime discovery via `which` 308 | let nixf_tidy_path = args 309 | .nixf_tidy_path 310 | .or(option_env!("NIXF_TIDY_PATH").map(|s| s.to_string())) 311 | .or(which("nixf-tidy").ok().map(|p| p.display().to_string())) 312 | .expect("nixf-tidy executable not found in PATH or --nixf-tidy-path not provided"); 313 | 314 | let files = args.files; 315 | let variable_lookup = args.variable_lookup; 316 | let auto_fix = args.auto_fix; 317 | let ignore = args.ignore; 318 | let only = args.only; 319 | 320 | let all_reports: Vec<_> = files 321 | .par_iter() 322 | .flat_map(|file| process_file(variable_lookup, &nixf_tidy_path, &ignore, &only, auto_fix, file)) 323 | .collect(); 324 | 325 | if !all_reports.is_empty() { 326 | for (report, input_file, source) in all_reports { 327 | report.eprint((input_file, source)).unwrap(); 328 | } 329 | std::process::exit(1); 330 | } 331 | } 332 | --------------------------------------------------------------------------------