├── .github └── workflows │ └── rust.yml ├── .gitignore ├── .tcount_queries ├── README.md ├── go │ ├── comment.scm │ ├── keyword.scm │ └── string_literal.scm ├── ruby │ ├── comment.scm │ ├── keyword.scm │ └── string_literal.scm └── rust │ ├── comment.scm │ ├── keyword.scm │ └── string_literal.scm ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── QUERIES.md ├── README.md ├── src ├── cli.rs ├── count.rs ├── error.rs ├── fs.rs ├── language.rs ├── main.rs ├── output.rs ├── query.rs └── tree.rs └── tests ├── fixtures ├── .ignore ├── .tcount_queries │ ├── README.md │ ├── go │ │ └── _test.scm │ ├── ruby │ │ └── _test.scm │ └── rust │ │ └── _test.scm ├── empty.rs ├── foo │ ├── empty.rs │ ├── invalid.rs │ ├── ruby.rb │ ├── ruby1.rb │ ├── rust1.rs │ ├── rust2.rs │ └── rust3.rs ├── go1.go ├── invalid.rs ├── ruby.rb ├── ruby1.rb ├── rust1.rs ├── rust2.rs ├── rust3.rs ├── unsupported.abc └── xdg_config_home │ └── tcount │ └── queries │ ├── go │ ├── _test.scm │ └── string.scm │ ├── ruby │ ├── _test.scm │ └── string.scm │ └── rust │ ├── _test.scm │ └── string.scm ├── main.rs ├── output.rs ├── query.rs └── utils └── mod.rs /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v2 19 | - name: Build 20 | run: cargo build --verbose 21 | - name: Run tests 22 | run: cargo test --verbose 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /treesitter 3 | -------------------------------------------------------------------------------- /.tcount_queries/README.md: -------------------------------------------------------------------------------- 1 | These are used in the `src/count.rs` tests. 2 | -------------------------------------------------------------------------------- /.tcount_queries/go/comment.scm: -------------------------------------------------------------------------------- 1 | (comment) 2 | -------------------------------------------------------------------------------- /.tcount_queries/go/keyword.scm: -------------------------------------------------------------------------------- 1 | [ 2 | "for" @repeat 3 | "if" @ifelse 4 | "else" @ifelse 5 | "break" 6 | "case" 7 | "chan" 8 | "const" 9 | "continue" 10 | "default" 11 | "defer" 12 | "fallthrough" 13 | "func" 14 | "go" 15 | "goto" 16 | "import" 17 | "interface" 18 | "map" 19 | "package" 20 | "range" 21 | "return" 22 | "select" 23 | "struct" 24 | "switch" 25 | "type" 26 | "var" 27 | ] 28 | -------------------------------------------------------------------------------- /.tcount_queries/go/string_literal.scm: -------------------------------------------------------------------------------- 1 | (interpreted_string_literal) 2 | (raw_string_literal) 3 | (rune_literal) 4 | -------------------------------------------------------------------------------- /.tcount_queries/ruby/comment.scm: -------------------------------------------------------------------------------- 1 | (comment) 2 | -------------------------------------------------------------------------------- /.tcount_queries/ruby/keyword.scm: -------------------------------------------------------------------------------- 1 | [ 2 | "for" @repeat 3 | "while" @repeat 4 | "until" @repeat 5 | "if" @ifelse 6 | "elsif" @ifelse 7 | "else" @ifelse 8 | (false) @boolean 9 | (true) @boolean 10 | (nil) @boolean 11 | "BEGIN" 12 | "END" 13 | "alias" 14 | "and" 15 | "begin" 16 | "break" 17 | "case" 18 | "class" 19 | "def" 20 | "defined?" 21 | "do" 22 | "end" 23 | "ensure" 24 | "in" 25 | "module" 26 | "next" 27 | "not" 28 | "or" 29 | "redo" 30 | "rescue" 31 | "retry" 32 | "return" 33 | (self) 34 | (super) 35 | "then" 36 | "undef" 37 | "unless" 38 | "when" 39 | "yield" 40 | ] 41 | -------------------------------------------------------------------------------- /.tcount_queries/ruby/string_literal.scm: -------------------------------------------------------------------------------- 1 | (string) 2 | (bare_string) 3 | (subshell) 4 | (heredoc_body) 5 | -------------------------------------------------------------------------------- /.tcount_queries/rust/comment.scm: -------------------------------------------------------------------------------- 1 | [ 2 | (line_comment) 3 | (block_comment) 4 | ] 5 | -------------------------------------------------------------------------------- /.tcount_queries/rust/keyword.scm: -------------------------------------------------------------------------------- 1 | [ 2 | "while" @repeat 3 | "for" @repeat 4 | "loop" @repeat 5 | "if" @ifelse 6 | "else" @ifelse 7 | "false" @boolean 8 | "true" @boolean 9 | "macro_rules!" @macro-definition 10 | "mod" @include 11 | "use" @include 12 | "async" 13 | "await" 14 | "as" 15 | "break" 16 | "const" 17 | "continue" 18 | (crate) 19 | "dyn" 20 | "enum" 21 | "extern" 22 | "fn" 23 | "impl" 24 | "in" 25 | "let" 26 | "match" 27 | "move" 28 | (mutable_specifier) 29 | "pub" 30 | "ref" 31 | "return" 32 | (self) 33 | "static" 34 | "struct" 35 | (super) 36 | "trait" 37 | "type" 38 | "union" 39 | "unsafe" 40 | "where" 41 | ] 42 | -------------------------------------------------------------------------------- /.tcount_queries/rust/string_literal.scm: -------------------------------------------------------------------------------- 1 | (char_literal) 2 | (string_literal) 3 | (raw_string_literal) 4 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "aho-corasick" 7 | version = "0.7.15" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "7404febffaa47dac81aa44dba71523c9d069b1bdc50a77db41195149e17f68e5" 10 | dependencies = [ 11 | "memchr", 12 | ] 13 | 14 | [[package]] 15 | name = "ansi_term" 16 | version = "0.11.0" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b" 19 | dependencies = [ 20 | "winapi", 21 | ] 22 | 23 | [[package]] 24 | name = "assert_cmd" 25 | version = "1.0.3" 26 | source = "registry+https://github.com/rust-lang/crates.io-index" 27 | checksum = "f2475b58cd94eb4f70159f4fd8844ba3b807532fe3131b3373fae060bbe30396" 28 | dependencies = [ 29 | "bstr", 30 | "doc-comment", 31 | "predicates", 32 | "predicates-core", 33 | "predicates-tree", 34 | "wait-timeout", 35 | ] 36 | 37 | [[package]] 38 | name = "atty" 39 | version = "0.2.14" 40 | source = "registry+https://github.com/rust-lang/crates.io-index" 41 | checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" 42 | dependencies = [ 43 | "hermit-abi 0.1.18", 44 | "libc", 45 | "winapi", 46 | ] 47 | 48 | [[package]] 49 | name = "autocfg" 50 | version = "1.0.1" 51 | source = "registry+https://github.com/rust-lang/crates.io-index" 52 | checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" 53 | 54 | [[package]] 55 | name = "bitflags" 56 | version = "1.2.1" 57 | source = "registry+https://github.com/rust-lang/crates.io-index" 58 | checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" 59 | 60 | [[package]] 61 | name = "bstr" 62 | version = "0.2.15" 63 | source = "registry+https://github.com/rust-lang/crates.io-index" 64 | checksum = "a40b47ad93e1a5404e6c18dec46b628214fee441c70f4ab5d6942142cc268a3d" 65 | dependencies = [ 66 | "lazy_static", 67 | "memchr", 68 | "regex-automata", 69 | "serde", 70 | ] 71 | 72 | [[package]] 73 | name = "byteorder" 74 | version = "1.4.3" 75 | source = "registry+https://github.com/rust-lang/crates.io-index" 76 | checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" 77 | 78 | [[package]] 79 | name = "cc" 80 | version = "1.0.67" 81 | source = "registry+https://github.com/rust-lang/crates.io-index" 82 | checksum = "e3c69b077ad434294d3ce9f1f6143a2a4b89a8a2d54ef813d85003a4fd1137fd" 83 | 84 | [[package]] 85 | name = "cfg-if" 86 | version = "1.0.0" 87 | source = "registry+https://github.com/rust-lang/crates.io-index" 88 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 89 | 90 | [[package]] 91 | name = "clap" 92 | version = "2.33.3" 93 | source = "registry+https://github.com/rust-lang/crates.io-index" 94 | checksum = "37e58ac78573c40708d45522f0d80fa2f01cc4f9b4e2bf749807255454312002" 95 | dependencies = [ 96 | "ansi_term", 97 | "atty", 98 | "bitflags", 99 | "strsim", 100 | "textwrap", 101 | "unicode-width", 102 | "vec_map", 103 | ] 104 | 105 | [[package]] 106 | name = "crossbeam-channel" 107 | version = "0.5.1" 108 | source = "registry+https://github.com/rust-lang/crates.io-index" 109 | checksum = "06ed27e177f16d65f0f0c22a213e17c696ace5dd64b14258b52f9417ccb52db4" 110 | dependencies = [ 111 | "cfg-if", 112 | "crossbeam-utils", 113 | ] 114 | 115 | [[package]] 116 | name = "crossbeam-deque" 117 | version = "0.8.0" 118 | source = "registry+https://github.com/rust-lang/crates.io-index" 119 | checksum = "94af6efb46fef72616855b036a624cf27ba656ffc9be1b9a3c931cfc7749a9a9" 120 | dependencies = [ 121 | "cfg-if", 122 | "crossbeam-epoch", 123 | "crossbeam-utils", 124 | ] 125 | 126 | [[package]] 127 | name = "crossbeam-epoch" 128 | version = "0.9.4" 129 | source = "registry+https://github.com/rust-lang/crates.io-index" 130 | checksum = "52fb27eab85b17fbb9f6fd667089e07d6a2eb8743d02639ee7f6a7a7729c9c94" 131 | dependencies = [ 132 | "cfg-if", 133 | "crossbeam-utils", 134 | "lazy_static", 135 | "memoffset", 136 | "scopeguard", 137 | ] 138 | 139 | [[package]] 140 | name = "crossbeam-utils" 141 | version = "0.8.4" 142 | source = "registry+https://github.com/rust-lang/crates.io-index" 143 | checksum = "4feb231f0d4d6af81aed15928e58ecf5816aa62a2393e2c82f46973e92a9a278" 144 | dependencies = [ 145 | "autocfg", 146 | "cfg-if", 147 | "lazy_static", 148 | ] 149 | 150 | [[package]] 151 | name = "csv" 152 | version = "1.1.6" 153 | source = "registry+https://github.com/rust-lang/crates.io-index" 154 | checksum = "22813a6dc45b335f9bade10bf7271dc477e81113e89eb251a0bc2a8a81c536e1" 155 | dependencies = [ 156 | "bstr", 157 | "csv-core", 158 | "itoa", 159 | "ryu", 160 | "serde", 161 | ] 162 | 163 | [[package]] 164 | name = "csv-core" 165 | version = "0.1.10" 166 | source = "registry+https://github.com/rust-lang/crates.io-index" 167 | checksum = "2b2466559f260f48ad25fe6317b3c8dac77b5bdb5763ac7d9d6103530663bc90" 168 | dependencies = [ 169 | "memchr", 170 | ] 171 | 172 | [[package]] 173 | name = "difference" 174 | version = "2.0.0" 175 | source = "registry+https://github.com/rust-lang/crates.io-index" 176 | checksum = "524cbf6897b527295dff137cec09ecf3a05f4fddffd7dfcd1585403449e74198" 177 | 178 | [[package]] 179 | name = "dirs-next" 180 | version = "2.0.0" 181 | source = "registry+https://github.com/rust-lang/crates.io-index" 182 | checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" 183 | dependencies = [ 184 | "cfg-if", 185 | "dirs-sys-next", 186 | ] 187 | 188 | [[package]] 189 | name = "dirs-sys-next" 190 | version = "0.1.2" 191 | source = "registry+https://github.com/rust-lang/crates.io-index" 192 | checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" 193 | dependencies = [ 194 | "libc", 195 | "redox_users", 196 | "winapi", 197 | ] 198 | 199 | [[package]] 200 | name = "doc-comment" 201 | version = "0.3.3" 202 | source = "registry+https://github.com/rust-lang/crates.io-index" 203 | checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" 204 | 205 | [[package]] 206 | name = "either" 207 | version = "1.6.1" 208 | source = "registry+https://github.com/rust-lang/crates.io-index" 209 | checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" 210 | 211 | [[package]] 212 | name = "encode_unicode" 213 | version = "1.0.0" 214 | source = "registry+https://github.com/rust-lang/crates.io-index" 215 | checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" 216 | 217 | [[package]] 218 | name = "errno" 219 | version = "0.3.1" 220 | source = "registry+https://github.com/rust-lang/crates.io-index" 221 | checksum = "4bcfec3a70f97c962c307b2d2c56e358cf1d00b558d74262b5f929ee8cc7e73a" 222 | dependencies = [ 223 | "errno-dragonfly", 224 | "libc", 225 | "windows-sys", 226 | ] 227 | 228 | [[package]] 229 | name = "errno-dragonfly" 230 | version = "0.1.2" 231 | source = "registry+https://github.com/rust-lang/crates.io-index" 232 | checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" 233 | dependencies = [ 234 | "cc", 235 | "libc", 236 | ] 237 | 238 | [[package]] 239 | name = "fnv" 240 | version = "1.0.7" 241 | source = "registry+https://github.com/rust-lang/crates.io-index" 242 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 243 | 244 | [[package]] 245 | name = "getrandom" 246 | version = "0.1.16" 247 | source = "registry+https://github.com/rust-lang/crates.io-index" 248 | checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" 249 | dependencies = [ 250 | "cfg-if", 251 | "libc", 252 | "wasi 0.9.0+wasi-snapshot-preview1", 253 | ] 254 | 255 | [[package]] 256 | name = "getrandom" 257 | version = "0.2.9" 258 | source = "registry+https://github.com/rust-lang/crates.io-index" 259 | checksum = "c85e1d9ab2eadba7e5040d4e09cbd6d072b76a557ad64e797c2cb9d4da21d7e4" 260 | dependencies = [ 261 | "cfg-if", 262 | "libc", 263 | "wasi 0.11.0+wasi-snapshot-preview1", 264 | ] 265 | 266 | [[package]] 267 | name = "glob" 268 | version = "0.3.0" 269 | source = "registry+https://github.com/rust-lang/crates.io-index" 270 | checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" 271 | 272 | [[package]] 273 | name = "globset" 274 | version = "0.4.6" 275 | source = "registry+https://github.com/rust-lang/crates.io-index" 276 | checksum = "c152169ef1e421390738366d2f796655fec62621dabbd0fd476f905934061e4a" 277 | dependencies = [ 278 | "aho-corasick", 279 | "bstr", 280 | "fnv", 281 | "log", 282 | "regex", 283 | ] 284 | 285 | [[package]] 286 | name = "heck" 287 | version = "0.3.2" 288 | source = "registry+https://github.com/rust-lang/crates.io-index" 289 | checksum = "87cbf45460356b7deeb5e3415b5563308c0a9b057c85e12b06ad551f98d0a6ac" 290 | dependencies = [ 291 | "unicode-segmentation", 292 | ] 293 | 294 | [[package]] 295 | name = "hermit-abi" 296 | version = "0.1.18" 297 | source = "registry+https://github.com/rust-lang/crates.io-index" 298 | checksum = "322f4de77956e22ed0e5032c359a0f1273f1f7f0d79bfa3b8ffbc730d7fbcc5c" 299 | dependencies = [ 300 | "libc", 301 | ] 302 | 303 | [[package]] 304 | name = "hermit-abi" 305 | version = "0.3.1" 306 | source = "registry+https://github.com/rust-lang/crates.io-index" 307 | checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286" 308 | 309 | [[package]] 310 | name = "ignore" 311 | version = "0.4.17" 312 | source = "registry+https://github.com/rust-lang/crates.io-index" 313 | checksum = "b287fb45c60bb826a0dc68ff08742b9d88a2fea13d6e0c286b3172065aaf878c" 314 | dependencies = [ 315 | "crossbeam-utils", 316 | "globset", 317 | "lazy_static", 318 | "log", 319 | "memchr", 320 | "regex", 321 | "same-file", 322 | "thread_local", 323 | "walkdir", 324 | "winapi-util", 325 | ] 326 | 327 | [[package]] 328 | name = "io-lifetimes" 329 | version = "1.0.10" 330 | source = "registry+https://github.com/rust-lang/crates.io-index" 331 | checksum = "9c66c74d2ae7e79a5a8f7ac924adbe38ee42a859c6539ad869eb51f0b52dc220" 332 | dependencies = [ 333 | "hermit-abi 0.3.1", 334 | "libc", 335 | "windows-sys", 336 | ] 337 | 338 | [[package]] 339 | name = "is-terminal" 340 | version = "0.4.7" 341 | source = "registry+https://github.com/rust-lang/crates.io-index" 342 | checksum = "adcf93614601c8129ddf72e2d5633df827ba6551541c6d8c59520a371475be1f" 343 | dependencies = [ 344 | "hermit-abi 0.3.1", 345 | "io-lifetimes", 346 | "rustix", 347 | "windows-sys", 348 | ] 349 | 350 | [[package]] 351 | name = "itoa" 352 | version = "0.4.7" 353 | source = "registry+https://github.com/rust-lang/crates.io-index" 354 | checksum = "dd25036021b0de88a0aff6b850051563c6516d0bf53f8638938edbb9de732736" 355 | 356 | [[package]] 357 | name = "lazy_static" 358 | version = "1.4.0" 359 | source = "registry+https://github.com/rust-lang/crates.io-index" 360 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 361 | 362 | [[package]] 363 | name = "libc" 364 | version = "0.2.141" 365 | source = "registry+https://github.com/rust-lang/crates.io-index" 366 | checksum = "3304a64d199bb964be99741b7a14d26972741915b3649639149b2479bb46f4b5" 367 | 368 | [[package]] 369 | name = "linux-raw-sys" 370 | version = "0.3.3" 371 | source = "registry+https://github.com/rust-lang/crates.io-index" 372 | checksum = "9b085a4f2cde5781fc4b1717f2e86c62f5cda49de7ba99a7c2eae02b61c9064c" 373 | 374 | [[package]] 375 | name = "log" 376 | version = "0.4.14" 377 | source = "registry+https://github.com/rust-lang/crates.io-index" 378 | checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" 379 | dependencies = [ 380 | "cfg-if", 381 | ] 382 | 383 | [[package]] 384 | name = "memchr" 385 | version = "2.3.4" 386 | source = "registry+https://github.com/rust-lang/crates.io-index" 387 | checksum = "0ee1c47aaa256ecabcaea351eae4a9b01ef39ed810004e298d2511ed284b1525" 388 | 389 | [[package]] 390 | name = "memoffset" 391 | version = "0.6.3" 392 | source = "registry+https://github.com/rust-lang/crates.io-index" 393 | checksum = "f83fb6581e8ed1f85fd45c116db8405483899489e38406156c25eb743554361d" 394 | dependencies = [ 395 | "autocfg", 396 | ] 397 | 398 | [[package]] 399 | name = "num_cpus" 400 | version = "1.13.0" 401 | source = "registry+https://github.com/rust-lang/crates.io-index" 402 | checksum = "05499f3756671c15885fee9034446956fff3f243d6077b91e5767df161f766b3" 403 | dependencies = [ 404 | "hermit-abi 0.1.18", 405 | "libc", 406 | ] 407 | 408 | [[package]] 409 | name = "once_cell" 410 | version = "1.7.2" 411 | source = "registry+https://github.com/rust-lang/crates.io-index" 412 | checksum = "af8b08b04175473088b46763e51ee54da5f9a164bc162f615b91bc179dbf15a3" 413 | 414 | [[package]] 415 | name = "phf" 416 | version = "0.8.0" 417 | source = "registry+https://github.com/rust-lang/crates.io-index" 418 | checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12" 419 | dependencies = [ 420 | "phf_macros", 421 | "phf_shared", 422 | "proc-macro-hack", 423 | ] 424 | 425 | [[package]] 426 | name = "phf_generator" 427 | version = "0.8.0" 428 | source = "registry+https://github.com/rust-lang/crates.io-index" 429 | checksum = "17367f0cc86f2d25802b2c26ee58a7b23faeccf78a396094c13dced0d0182526" 430 | dependencies = [ 431 | "phf_shared", 432 | "rand", 433 | ] 434 | 435 | [[package]] 436 | name = "phf_macros" 437 | version = "0.8.0" 438 | source = "registry+https://github.com/rust-lang/crates.io-index" 439 | checksum = "7f6fde18ff429ffc8fe78e2bf7f8b7a5a5a6e2a8b58bc5a9ac69198bbda9189c" 440 | dependencies = [ 441 | "phf_generator", 442 | "phf_shared", 443 | "proc-macro-hack", 444 | "proc-macro2", 445 | "quote", 446 | "syn", 447 | ] 448 | 449 | [[package]] 450 | name = "phf_shared" 451 | version = "0.8.0" 452 | source = "registry+https://github.com/rust-lang/crates.io-index" 453 | checksum = "c00cf8b9eafe68dde5e9eaa2cef8ee84a9336a47d566ec55ca16589633b65af7" 454 | dependencies = [ 455 | "siphasher", 456 | ] 457 | 458 | [[package]] 459 | name = "ppv-lite86" 460 | version = "0.2.10" 461 | source = "registry+https://github.com/rust-lang/crates.io-index" 462 | checksum = "ac74c624d6b2d21f425f752262f42188365d7b8ff1aff74c82e45136510a4857" 463 | 464 | [[package]] 465 | name = "predicates" 466 | version = "1.0.8" 467 | source = "registry+https://github.com/rust-lang/crates.io-index" 468 | checksum = "f49cfaf7fdaa3bfacc6fa3e7054e65148878354a5cfddcf661df4c851f8021df" 469 | dependencies = [ 470 | "difference", 471 | "predicates-core", 472 | ] 473 | 474 | [[package]] 475 | name = "predicates-core" 476 | version = "1.0.2" 477 | source = "registry+https://github.com/rust-lang/crates.io-index" 478 | checksum = "57e35a3326b75e49aa85f5dc6ec15b41108cf5aee58eabb1f274dd18b73c2451" 479 | 480 | [[package]] 481 | name = "predicates-tree" 482 | version = "1.0.2" 483 | source = "registry+https://github.com/rust-lang/crates.io-index" 484 | checksum = "15f553275e5721409451eb85e15fd9a860a6e5ab4496eb215987502b5f5391f2" 485 | dependencies = [ 486 | "predicates-core", 487 | "treeline", 488 | ] 489 | 490 | [[package]] 491 | name = "prettytable-rs" 492 | version = "0.10.0" 493 | source = "registry+https://github.com/rust-lang/crates.io-index" 494 | checksum = "eea25e07510aa6ab6547308ebe3c036016d162b8da920dbb079e3ba8acf3d95a" 495 | dependencies = [ 496 | "csv", 497 | "encode_unicode", 498 | "is-terminal", 499 | "lazy_static", 500 | "term", 501 | "unicode-width", 502 | ] 503 | 504 | [[package]] 505 | name = "proc-macro-error" 506 | version = "1.0.4" 507 | source = "registry+https://github.com/rust-lang/crates.io-index" 508 | checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" 509 | dependencies = [ 510 | "proc-macro-error-attr", 511 | "proc-macro2", 512 | "quote", 513 | "syn", 514 | "version_check", 515 | ] 516 | 517 | [[package]] 518 | name = "proc-macro-error-attr" 519 | version = "1.0.4" 520 | source = "registry+https://github.com/rust-lang/crates.io-index" 521 | checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" 522 | dependencies = [ 523 | "proc-macro2", 524 | "quote", 525 | "version_check", 526 | ] 527 | 528 | [[package]] 529 | name = "proc-macro-hack" 530 | version = "0.5.19" 531 | source = "registry+https://github.com/rust-lang/crates.io-index" 532 | checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5" 533 | 534 | [[package]] 535 | name = "proc-macro2" 536 | version = "1.0.26" 537 | source = "registry+https://github.com/rust-lang/crates.io-index" 538 | checksum = "a152013215dca273577e18d2bf00fa862b89b24169fb78c4c95aeb07992c9cec" 539 | dependencies = [ 540 | "unicode-xid", 541 | ] 542 | 543 | [[package]] 544 | name = "quote" 545 | version = "1.0.9" 546 | source = "registry+https://github.com/rust-lang/crates.io-index" 547 | checksum = "c3d0b9745dc2debf507c8422de05d7226cc1f0644216dfdfead988f9b1ab32a7" 548 | dependencies = [ 549 | "proc-macro2", 550 | ] 551 | 552 | [[package]] 553 | name = "rand" 554 | version = "0.7.3" 555 | source = "registry+https://github.com/rust-lang/crates.io-index" 556 | checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" 557 | dependencies = [ 558 | "getrandom 0.1.16", 559 | "libc", 560 | "rand_chacha", 561 | "rand_core", 562 | "rand_hc", 563 | "rand_pcg", 564 | ] 565 | 566 | [[package]] 567 | name = "rand_chacha" 568 | version = "0.2.2" 569 | source = "registry+https://github.com/rust-lang/crates.io-index" 570 | checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" 571 | dependencies = [ 572 | "ppv-lite86", 573 | "rand_core", 574 | ] 575 | 576 | [[package]] 577 | name = "rand_core" 578 | version = "0.5.1" 579 | source = "registry+https://github.com/rust-lang/crates.io-index" 580 | checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" 581 | dependencies = [ 582 | "getrandom 0.1.16", 583 | ] 584 | 585 | [[package]] 586 | name = "rand_hc" 587 | version = "0.2.0" 588 | source = "registry+https://github.com/rust-lang/crates.io-index" 589 | checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" 590 | dependencies = [ 591 | "rand_core", 592 | ] 593 | 594 | [[package]] 595 | name = "rand_pcg" 596 | version = "0.2.1" 597 | source = "registry+https://github.com/rust-lang/crates.io-index" 598 | checksum = "16abd0c1b639e9eb4d7c50c0b8100b0d0f849be2349829c740fe8e6eb4816429" 599 | dependencies = [ 600 | "rand_core", 601 | ] 602 | 603 | [[package]] 604 | name = "rayon" 605 | version = "1.5.0" 606 | source = "registry+https://github.com/rust-lang/crates.io-index" 607 | checksum = "8b0d8e0819fadc20c74ea8373106ead0600e3a67ef1fe8da56e39b9ae7275674" 608 | dependencies = [ 609 | "autocfg", 610 | "crossbeam-deque", 611 | "either", 612 | "rayon-core", 613 | ] 614 | 615 | [[package]] 616 | name = "rayon-core" 617 | version = "1.9.0" 618 | source = "registry+https://github.com/rust-lang/crates.io-index" 619 | checksum = "9ab346ac5921dc62ffa9f89b7a773907511cdfa5490c572ae9be1be33e8afa4a" 620 | dependencies = [ 621 | "crossbeam-channel", 622 | "crossbeam-deque", 623 | "crossbeam-utils", 624 | "lazy_static", 625 | "num_cpus", 626 | ] 627 | 628 | [[package]] 629 | name = "redox_syscall" 630 | version = "0.2.16" 631 | source = "registry+https://github.com/rust-lang/crates.io-index" 632 | checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" 633 | dependencies = [ 634 | "bitflags", 635 | ] 636 | 637 | [[package]] 638 | name = "redox_users" 639 | version = "0.4.3" 640 | source = "registry+https://github.com/rust-lang/crates.io-index" 641 | checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" 642 | dependencies = [ 643 | "getrandom 0.2.9", 644 | "redox_syscall", 645 | "thiserror", 646 | ] 647 | 648 | [[package]] 649 | name = "regex" 650 | version = "1.4.6" 651 | source = "registry+https://github.com/rust-lang/crates.io-index" 652 | checksum = "2a26af418b574bd56588335b3a3659a65725d4e636eb1016c2f9e3b38c7cc759" 653 | dependencies = [ 654 | "aho-corasick", 655 | "memchr", 656 | "regex-syntax", 657 | ] 658 | 659 | [[package]] 660 | name = "regex-automata" 661 | version = "0.1.9" 662 | source = "registry+https://github.com/rust-lang/crates.io-index" 663 | checksum = "ae1ded71d66a4a97f5e961fd0cb25a5f366a42a41570d16a763a69c092c26ae4" 664 | dependencies = [ 665 | "byteorder", 666 | ] 667 | 668 | [[package]] 669 | name = "regex-syntax" 670 | version = "0.6.23" 671 | source = "registry+https://github.com/rust-lang/crates.io-index" 672 | checksum = "24d5f089152e60f62d28b835fbff2cd2e8dc0baf1ac13343bef92ab7eed84548" 673 | 674 | [[package]] 675 | name = "rustix" 676 | version = "0.37.12" 677 | source = "registry+https://github.com/rust-lang/crates.io-index" 678 | checksum = "722529a737f5a942fdbac3a46cee213053196737c5eaa3386d52e85b786f2659" 679 | dependencies = [ 680 | "bitflags", 681 | "errno", 682 | "io-lifetimes", 683 | "libc", 684 | "linux-raw-sys", 685 | "windows-sys", 686 | ] 687 | 688 | [[package]] 689 | name = "rustversion" 690 | version = "1.0.12" 691 | source = "registry+https://github.com/rust-lang/crates.io-index" 692 | checksum = "4f3208ce4d8448b3f3e7d168a73f5e0c43a61e32930de3bceeccedb388b6bf06" 693 | 694 | [[package]] 695 | name = "ryu" 696 | version = "1.0.5" 697 | source = "registry+https://github.com/rust-lang/crates.io-index" 698 | checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e" 699 | 700 | [[package]] 701 | name = "same-file" 702 | version = "1.0.6" 703 | source = "registry+https://github.com/rust-lang/crates.io-index" 704 | checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" 705 | dependencies = [ 706 | "winapi-util", 707 | ] 708 | 709 | [[package]] 710 | name = "scopeguard" 711 | version = "1.1.0" 712 | source = "registry+https://github.com/rust-lang/crates.io-index" 713 | checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" 714 | 715 | [[package]] 716 | name = "serde" 717 | version = "1.0.125" 718 | source = "registry+https://github.com/rust-lang/crates.io-index" 719 | checksum = "558dc50e1a5a5fa7112ca2ce4effcb321b0300c0d4ccf0776a9f60cd89031171" 720 | 721 | [[package]] 722 | name = "siphasher" 723 | version = "0.3.5" 724 | source = "registry+https://github.com/rust-lang/crates.io-index" 725 | checksum = "cbce6d4507c7e4a3962091436e56e95290cb71fa302d0d270e32130b75fbff27" 726 | 727 | [[package]] 728 | name = "strsim" 729 | version = "0.8.0" 730 | source = "registry+https://github.com/rust-lang/crates.io-index" 731 | checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" 732 | 733 | [[package]] 734 | name = "structopt" 735 | version = "0.3.21" 736 | source = "registry+https://github.com/rust-lang/crates.io-index" 737 | checksum = "5277acd7ee46e63e5168a80734c9f6ee81b1367a7d8772a2d765df2a3705d28c" 738 | dependencies = [ 739 | "clap", 740 | "lazy_static", 741 | "structopt-derive", 742 | ] 743 | 744 | [[package]] 745 | name = "structopt-derive" 746 | version = "0.4.14" 747 | source = "registry+https://github.com/rust-lang/crates.io-index" 748 | checksum = "5ba9cdfda491b814720b6b06e0cac513d922fc407582032e8706e9f137976f90" 749 | dependencies = [ 750 | "heck", 751 | "proc-macro-error", 752 | "proc-macro2", 753 | "quote", 754 | "syn", 755 | ] 756 | 757 | [[package]] 758 | name = "syn" 759 | version = "1.0.70" 760 | source = "registry+https://github.com/rust-lang/crates.io-index" 761 | checksum = "b9505f307c872bab8eb46f77ae357c8eba1fdacead58ee5a850116b1d7f82883" 762 | dependencies = [ 763 | "proc-macro2", 764 | "quote", 765 | "unicode-xid", 766 | ] 767 | 768 | [[package]] 769 | name = "tcount" 770 | version = "0.1.0" 771 | dependencies = [ 772 | "assert_cmd", 773 | "cc", 774 | "glob", 775 | "ignore", 776 | "phf", 777 | "prettytable-rs", 778 | "rayon", 779 | "regex", 780 | "structopt", 781 | "tree-sitter", 782 | "tree-sitter-bash", 783 | "tree-sitter-bibtex", 784 | "tree-sitter-c", 785 | "tree-sitter-c-sharp", 786 | "tree-sitter-clojure", 787 | "tree-sitter-cpp", 788 | "tree-sitter-css", 789 | "tree-sitter-elm", 790 | "tree-sitter-embedded-template", 791 | "tree-sitter-erlang", 792 | "tree-sitter-go", 793 | "tree-sitter-html", 794 | "tree-sitter-java", 795 | "tree-sitter-javascript", 796 | "tree-sitter-json", 797 | "tree-sitter-julia", 798 | "tree-sitter-latex", 799 | "tree-sitter-lua", 800 | "tree-sitter-markdown", 801 | "tree-sitter-ocaml", 802 | "tree-sitter-python", 803 | "tree-sitter-query", 804 | "tree-sitter-ruby", 805 | "tree-sitter-rust", 806 | "tree-sitter-scala", 807 | "tree-sitter-svelte", 808 | "tree-sitter-typescript", 809 | ] 810 | 811 | [[package]] 812 | name = "term" 813 | version = "0.7.0" 814 | source = "registry+https://github.com/rust-lang/crates.io-index" 815 | checksum = "c59df8ac95d96ff9bede18eb7300b0fda5e5d8d90960e76f8e14ae765eedbf1f" 816 | dependencies = [ 817 | "dirs-next", 818 | "rustversion", 819 | "winapi", 820 | ] 821 | 822 | [[package]] 823 | name = "textwrap" 824 | version = "0.11.0" 825 | source = "registry+https://github.com/rust-lang/crates.io-index" 826 | checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" 827 | dependencies = [ 828 | "unicode-width", 829 | ] 830 | 831 | [[package]] 832 | name = "thiserror" 833 | version = "1.0.39" 834 | source = "registry+https://github.com/rust-lang/crates.io-index" 835 | checksum = "a5ab016db510546d856297882807df8da66a16fb8c4101cb8b30054b0d5b2d9c" 836 | dependencies = [ 837 | "thiserror-impl", 838 | ] 839 | 840 | [[package]] 841 | name = "thiserror-impl" 842 | version = "1.0.39" 843 | source = "registry+https://github.com/rust-lang/crates.io-index" 844 | checksum = "5420d42e90af0c38c3290abcca25b9b3bdf379fc9f55c528f53a269d9c9a267e" 845 | dependencies = [ 846 | "proc-macro2", 847 | "quote", 848 | "syn", 849 | ] 850 | 851 | [[package]] 852 | name = "thread_local" 853 | version = "1.1.3" 854 | source = "registry+https://github.com/rust-lang/crates.io-index" 855 | checksum = "8018d24e04c95ac8790716a5987d0fec4f8b27249ffa0f7d33f1369bdfb88cbd" 856 | dependencies = [ 857 | "once_cell", 858 | ] 859 | 860 | [[package]] 861 | name = "tree-sitter" 862 | version = "0.19.3" 863 | source = "registry+https://github.com/rust-lang/crates.io-index" 864 | checksum = "1f41201fed3db3b520405a9c01c61773a250d4c3f43e9861c14b2bb232c981ab" 865 | dependencies = [ 866 | "cc", 867 | "regex", 868 | ] 869 | 870 | [[package]] 871 | name = "tree-sitter-bash" 872 | version = "0.19.0" 873 | source = "registry+https://github.com/rust-lang/crates.io-index" 874 | checksum = "c629e2d29ebb85b34cd195a1c511a161ed775451456cde110470e7af693424db" 875 | dependencies = [ 876 | "cc", 877 | "tree-sitter", 878 | ] 879 | 880 | [[package]] 881 | name = "tree-sitter-bibtex" 882 | version = "0.0.1" 883 | source = "git+https://github.com/latex-lsp/tree-sitter-bibtex#ccfd77db0ed799b6c22c214fe9d2937f47bc8b34" 884 | dependencies = [ 885 | "cc", 886 | "tree-sitter", 887 | ] 888 | 889 | [[package]] 890 | name = "tree-sitter-c" 891 | version = "0.16.0" 892 | source = "git+https://github.com/tree-sitter/tree-sitter-c#f05e279aedde06a25801c3f2b2cc8ac17fac52ae" 893 | dependencies = [ 894 | "cc", 895 | "tree-sitter", 896 | ] 897 | 898 | [[package]] 899 | name = "tree-sitter-c-sharp" 900 | version = "0.19.0" 901 | source = "git+https://github.com/tree-sitter/tree-sitter-c-sharp#851ac4735f66ec9c479096cc21bf58519da49faa" 902 | dependencies = [ 903 | "cc", 904 | "tree-sitter", 905 | ] 906 | 907 | [[package]] 908 | name = "tree-sitter-clojure" 909 | version = "0.0.8" 910 | source = "git+https://github.com/sogaiu/tree-sitter-clojure#f7d100c4fbaa8aad537e80c7974c470c7fb6aeda" 911 | dependencies = [ 912 | "cc", 913 | "tree-sitter", 914 | ] 915 | 916 | [[package]] 917 | name = "tree-sitter-cpp" 918 | version = "0.19.0" 919 | source = "registry+https://github.com/rust-lang/crates.io-index" 920 | checksum = "c7bd90c7b7db59369ed00fbc40458d9c9b2b8ed145640e337e839ac07aa63e15" 921 | dependencies = [ 922 | "cc", 923 | "tree-sitter", 924 | ] 925 | 926 | [[package]] 927 | name = "tree-sitter-css" 928 | version = "0.19.0" 929 | source = "git+https://github.com/tree-sitter/tree-sitter-css#94e10230939e702b4fa3fa2cb5c3bc7173b95d07" 930 | dependencies = [ 931 | "cc", 932 | "tree-sitter", 933 | ] 934 | 935 | [[package]] 936 | name = "tree-sitter-elm" 937 | version = "5.3.3" 938 | source = "registry+https://github.com/rust-lang/crates.io-index" 939 | checksum = "36bfeebf505b6cbbb0100ec2d8d725c053728a53570154ce76ebe77dfc3bafeb" 940 | dependencies = [ 941 | "cc", 942 | "tree-sitter", 943 | ] 944 | 945 | [[package]] 946 | name = "tree-sitter-embedded-template" 947 | version = "0.19.0" 948 | source = "registry+https://github.com/rust-lang/crates.io-index" 949 | checksum = "193fe1e3b39f71785d1890e4ac7bedd9be3d797f5db6363884415804fd516d93" 950 | dependencies = [ 951 | "cc", 952 | "tree-sitter", 953 | ] 954 | 955 | [[package]] 956 | name = "tree-sitter-erlang" 957 | version = "0.0.1" 958 | source = "git+https://github.com/AbstractMachinesLab/tree-sitter-erlang?branch=main#fea0141fb1e6d3af45c3d8f45441766793ed6e30" 959 | dependencies = [ 960 | "cc", 961 | "tree-sitter", 962 | ] 963 | 964 | [[package]] 965 | name = "tree-sitter-go" 966 | version = "0.19.0" 967 | source = "git+https://github.com/tree-sitter/tree-sitter-go#2a2fbf271ad6b864202f97101a2809009957535e" 968 | dependencies = [ 969 | "cc", 970 | "tree-sitter", 971 | ] 972 | 973 | [[package]] 974 | name = "tree-sitter-html" 975 | version = "0.19.0" 976 | source = "git+https://github.com/tree-sitter/tree-sitter-html#d93af487cc75120c89257195e6be46c999c6ba18" 977 | dependencies = [ 978 | "cc", 979 | "tree-sitter", 980 | ] 981 | 982 | [[package]] 983 | name = "tree-sitter-java" 984 | version = "0.19.0" 985 | source = "registry+https://github.com/rust-lang/crates.io-index" 986 | checksum = "301ae2ee7813e1bf935dc06db947642400645bbea8878431e1b31131488d5430" 987 | dependencies = [ 988 | "cc", 989 | "tree-sitter", 990 | ] 991 | 992 | [[package]] 993 | name = "tree-sitter-javascript" 994 | version = "0.19.0" 995 | source = "registry+https://github.com/rust-lang/crates.io-index" 996 | checksum = "840bb4d5f3c384cb76b976ff07297f5a24b6e61a708baa4464f53e395caaa5f9" 997 | dependencies = [ 998 | "cc", 999 | "tree-sitter", 1000 | ] 1001 | 1002 | [[package]] 1003 | name = "tree-sitter-json" 1004 | version = "0.19.0" 1005 | source = "git+https://github.com/tree-sitter/tree-sitter-json#65bceef69c3b0f24c0b19ce67d79f57c96e90fcb" 1006 | dependencies = [ 1007 | "cc", 1008 | "tree-sitter", 1009 | ] 1010 | 1011 | [[package]] 1012 | name = "tree-sitter-julia" 1013 | version = "0.19.0" 1014 | source = "git+https://github.com/tree-sitter/tree-sitter-julia#0ba7a24b062b671263ae08e707e9e94383b25bb7" 1015 | dependencies = [ 1016 | "cc", 1017 | "tree-sitter", 1018 | ] 1019 | 1020 | [[package]] 1021 | name = "tree-sitter-latex" 1022 | version = "0.0.1" 1023 | source = "git+https://github.com/latex-lsp/tree-sitter-latex#ea43db6830632fd3531b9cbc34a93502b0d4339a" 1024 | dependencies = [ 1025 | "cc", 1026 | "tree-sitter", 1027 | ] 1028 | 1029 | [[package]] 1030 | name = "tree-sitter-lua" 1031 | version = "0.0.1" 1032 | source = "git+https://github.com/nvim-treesitter/tree-sitter-lua#b6d4e9e10ccb7b3afb45018fbc391b4439306b23" 1033 | dependencies = [ 1034 | "cc", 1035 | "tree-sitter", 1036 | ] 1037 | 1038 | [[package]] 1039 | name = "tree-sitter-markdown" 1040 | version = "0.7.1" 1041 | source = "registry+https://github.com/rust-lang/crates.io-index" 1042 | checksum = "c7c1cfe0396b0e500cc99067bcd2d48720eb08a077e2690194f0c3a3d105f9a0" 1043 | dependencies = [ 1044 | "cc", 1045 | "tree-sitter", 1046 | ] 1047 | 1048 | [[package]] 1049 | name = "tree-sitter-ocaml" 1050 | version = "0.19.0" 1051 | source = "registry+https://github.com/rust-lang/crates.io-index" 1052 | checksum = "0d881da432193e7eea77c8973112d0f58f34d019e6b6be64c341b8e9214aaa07" 1053 | dependencies = [ 1054 | "cc", 1055 | "tree-sitter", 1056 | ] 1057 | 1058 | [[package]] 1059 | name = "tree-sitter-python" 1060 | version = "0.19.0" 1061 | source = "registry+https://github.com/rust-lang/crates.io-index" 1062 | checksum = "5646bfe71c4eb1c21b714ce0c38334c311eab767095582859e85da6281e9fd6c" 1063 | dependencies = [ 1064 | "cc", 1065 | "tree-sitter", 1066 | ] 1067 | 1068 | [[package]] 1069 | name = "tree-sitter-query" 1070 | version = "0.0.1" 1071 | source = "git+https://github.com/nvim-treesitter/tree-sitter-query#bc753fa4d8349bd6280f634f57bd6e7be9a3ed17" 1072 | dependencies = [ 1073 | "cc", 1074 | "tree-sitter", 1075 | ] 1076 | 1077 | [[package]] 1078 | name = "tree-sitter-ruby" 1079 | version = "0.19.0" 1080 | source = "git+https://github.com/tree-sitter/tree-sitter-ruby#963b7b8b20c6cf30a4a12a5d2a58b5fe0a482558" 1081 | dependencies = [ 1082 | "cc", 1083 | "tree-sitter", 1084 | ] 1085 | 1086 | [[package]] 1087 | name = "tree-sitter-rust" 1088 | version = "0.19.0" 1089 | source = "registry+https://github.com/rust-lang/crates.io-index" 1090 | checksum = "784f7ef9cdbd4c895dc2d4bb785e95b4a5364a602eec803681db83d1927ddf15" 1091 | dependencies = [ 1092 | "cc", 1093 | "tree-sitter", 1094 | ] 1095 | 1096 | [[package]] 1097 | name = "tree-sitter-scala" 1098 | version = "0.19.0" 1099 | source = "git+https://github.com/tree-sitter/tree-sitter-scala#fb23ed9a99da012d86b7a5059b9d8928607cce29" 1100 | dependencies = [ 1101 | "cc", 1102 | "tree-sitter", 1103 | ] 1104 | 1105 | [[package]] 1106 | name = "tree-sitter-svelte" 1107 | version = "0.8.1" 1108 | source = "registry+https://github.com/rust-lang/crates.io-index" 1109 | checksum = "6dfa5b26f6d47737e8ed37d48782030665beabb88f8a80540cc67756eca38266" 1110 | dependencies = [ 1111 | "cc", 1112 | "tree-sitter", 1113 | ] 1114 | 1115 | [[package]] 1116 | name = "tree-sitter-typescript" 1117 | version = "0.19.0" 1118 | source = "registry+https://github.com/rust-lang/crates.io-index" 1119 | checksum = "d3f62d49c6e56bf291c412ee5e178ea14dff40f14a5f01a8847933f56d65bf3b" 1120 | dependencies = [ 1121 | "cc", 1122 | "tree-sitter", 1123 | ] 1124 | 1125 | [[package]] 1126 | name = "treeline" 1127 | version = "0.1.0" 1128 | source = "registry+https://github.com/rust-lang/crates.io-index" 1129 | checksum = "a7f741b240f1a48843f9b8e0444fb55fb2a4ff67293b50a9179dfd5ea67f8d41" 1130 | 1131 | [[package]] 1132 | name = "unicode-segmentation" 1133 | version = "1.7.1" 1134 | source = "registry+https://github.com/rust-lang/crates.io-index" 1135 | checksum = "bb0d2e7be6ae3a5fa87eed5fb451aff96f2573d2694942e40543ae0bbe19c796" 1136 | 1137 | [[package]] 1138 | name = "unicode-width" 1139 | version = "0.1.8" 1140 | source = "registry+https://github.com/rust-lang/crates.io-index" 1141 | checksum = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3" 1142 | 1143 | [[package]] 1144 | name = "unicode-xid" 1145 | version = "0.2.1" 1146 | source = "registry+https://github.com/rust-lang/crates.io-index" 1147 | checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564" 1148 | 1149 | [[package]] 1150 | name = "vec_map" 1151 | version = "0.8.2" 1152 | source = "registry+https://github.com/rust-lang/crates.io-index" 1153 | checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" 1154 | 1155 | [[package]] 1156 | name = "version_check" 1157 | version = "0.9.3" 1158 | source = "registry+https://github.com/rust-lang/crates.io-index" 1159 | checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe" 1160 | 1161 | [[package]] 1162 | name = "wait-timeout" 1163 | version = "0.2.0" 1164 | source = "registry+https://github.com/rust-lang/crates.io-index" 1165 | checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6" 1166 | dependencies = [ 1167 | "libc", 1168 | ] 1169 | 1170 | [[package]] 1171 | name = "walkdir" 1172 | version = "2.3.2" 1173 | source = "registry+https://github.com/rust-lang/crates.io-index" 1174 | checksum = "808cf2735cd4b6866113f648b791c6adc5714537bc222d9347bb203386ffda56" 1175 | dependencies = [ 1176 | "same-file", 1177 | "winapi", 1178 | "winapi-util", 1179 | ] 1180 | 1181 | [[package]] 1182 | name = "wasi" 1183 | version = "0.9.0+wasi-snapshot-preview1" 1184 | source = "registry+https://github.com/rust-lang/crates.io-index" 1185 | checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" 1186 | 1187 | [[package]] 1188 | name = "wasi" 1189 | version = "0.11.0+wasi-snapshot-preview1" 1190 | source = "registry+https://github.com/rust-lang/crates.io-index" 1191 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 1192 | 1193 | [[package]] 1194 | name = "winapi" 1195 | version = "0.3.9" 1196 | source = "registry+https://github.com/rust-lang/crates.io-index" 1197 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 1198 | dependencies = [ 1199 | "winapi-i686-pc-windows-gnu", 1200 | "winapi-x86_64-pc-windows-gnu", 1201 | ] 1202 | 1203 | [[package]] 1204 | name = "winapi-i686-pc-windows-gnu" 1205 | version = "0.4.0" 1206 | source = "registry+https://github.com/rust-lang/crates.io-index" 1207 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 1208 | 1209 | [[package]] 1210 | name = "winapi-util" 1211 | version = "0.1.5" 1212 | source = "registry+https://github.com/rust-lang/crates.io-index" 1213 | checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" 1214 | dependencies = [ 1215 | "winapi", 1216 | ] 1217 | 1218 | [[package]] 1219 | name = "winapi-x86_64-pc-windows-gnu" 1220 | version = "0.4.0" 1221 | source = "registry+https://github.com/rust-lang/crates.io-index" 1222 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 1223 | 1224 | [[package]] 1225 | name = "windows-sys" 1226 | version = "0.48.0" 1227 | source = "registry+https://github.com/rust-lang/crates.io-index" 1228 | checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" 1229 | dependencies = [ 1230 | "windows-targets", 1231 | ] 1232 | 1233 | [[package]] 1234 | name = "windows-targets" 1235 | version = "0.48.0" 1236 | source = "registry+https://github.com/rust-lang/crates.io-index" 1237 | checksum = "7b1eb6f0cd7c80c79759c929114ef071b87354ce476d9d94271031c0497adfd5" 1238 | dependencies = [ 1239 | "windows_aarch64_gnullvm", 1240 | "windows_aarch64_msvc", 1241 | "windows_i686_gnu", 1242 | "windows_i686_msvc", 1243 | "windows_x86_64_gnu", 1244 | "windows_x86_64_gnullvm", 1245 | "windows_x86_64_msvc", 1246 | ] 1247 | 1248 | [[package]] 1249 | name = "windows_aarch64_gnullvm" 1250 | version = "0.48.0" 1251 | source = "registry+https://github.com/rust-lang/crates.io-index" 1252 | checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" 1253 | 1254 | [[package]] 1255 | name = "windows_aarch64_msvc" 1256 | version = "0.48.0" 1257 | source = "registry+https://github.com/rust-lang/crates.io-index" 1258 | checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" 1259 | 1260 | [[package]] 1261 | name = "windows_i686_gnu" 1262 | version = "0.48.0" 1263 | source = "registry+https://github.com/rust-lang/crates.io-index" 1264 | checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" 1265 | 1266 | [[package]] 1267 | name = "windows_i686_msvc" 1268 | version = "0.48.0" 1269 | source = "registry+https://github.com/rust-lang/crates.io-index" 1270 | checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" 1271 | 1272 | [[package]] 1273 | name = "windows_x86_64_gnu" 1274 | version = "0.48.0" 1275 | source = "registry+https://github.com/rust-lang/crates.io-index" 1276 | checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" 1277 | 1278 | [[package]] 1279 | name = "windows_x86_64_gnullvm" 1280 | version = "0.48.0" 1281 | source = "registry+https://github.com/rust-lang/crates.io-index" 1282 | checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" 1283 | 1284 | [[package]] 1285 | name = "windows_x86_64_msvc" 1286 | version = "0.48.0" 1287 | source = "registry+https://github.com/rust-lang/crates.io-index" 1288 | checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" 1289 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "tcount" 3 | version = "0.1.0" 4 | authors = ["Adam P. Regasz-Rethy "] 5 | edition = "2021" 6 | 7 | [build-dependencies] 8 | cc="*" 9 | 10 | [dev-dependencies] 11 | assert_cmd = "1.0.3" 12 | 13 | # Commented out crates are parsers which are out of date 14 | [dependencies] 15 | structopt = "0.3" 16 | regex = "1" 17 | phf = { version = "0.8", features = ["macros"] } 18 | ignore = "0.4.17" 19 | rayon = "1.5.0" 20 | glob = "0.3.0" 21 | prettytable-rs = "^0.10" 22 | tree-sitter = "0.19.3" 23 | tree-sitter-bash = "0.19.0" 24 | tree-sitter-bibtex = { git = "https://github.com/latex-lsp/tree-sitter-bibtex", version = "0.0.1" } 25 | tree-sitter-c = { git = "https://github.com/tree-sitter/tree-sitter-c", version = "0.16.0" } 26 | tree-sitter-c-sharp = { git = "https://github.com/tree-sitter/tree-sitter-c-sharp", version = "0.19.0" } 27 | tree-sitter-clojure = { git = "https://github.com/sogaiu/tree-sitter-clojure", version = "0.0.8" } 28 | tree-sitter-cpp = "0.19.0" 29 | tree-sitter-css = { git = "https://github.com/tree-sitter/tree-sitter-css", version = "0.19.0" } 30 | # tree-sitter-dart = { git = "https://github.com/RRethy/tree-sitter-dart", version = "*" } 31 | tree-sitter-elm = "5.3.3" 32 | tree-sitter-erlang = { git = "https://github.com/AbstractMachinesLab/tree-sitter-erlang", version = "0.0.1", branch = "main" } 33 | tree-sitter-embedded-template = "0.19.0" 34 | # tree-sitter-fennel = { git = "https://github.com/RRethy/tree-sitter-fennel", version = "0.0.1" } 35 | # tree-sitter-gdscript = { git = "https://github.com/PrestonKnopp/tree-sitter-gdscript", version = "*" } 36 | tree-sitter-go = { git = "https://github.com/tree-sitter/tree-sitter-go", version = "0.19.0" } 37 | # tree-sitter-graphql = { git = "https://github.com/bkegley/tree-sitter-graphql", version = "0.0.1" } 38 | # tree-sitter-haskell = { git = "https://github.com/tree-sitter/tree-sitter-haskell", version = "0.14.0" } 39 | tree-sitter-html = { git = "https://github.com/tree-sitter/tree-sitter-html", version = "0.19.0" } 40 | tree-sitter-java = "0.19.0" 41 | tree-sitter-javascript = "0.19.0" 42 | tree-sitter-json = { git = "https://github.com/tree-sitter/tree-sitter-json", version = "0.19.0" } 43 | tree-sitter-julia = { git = "https://github.com/tree-sitter/tree-sitter-julia", version = "0.19.0" } 44 | # tree-sitter-kotlin = { git = "https://github.com/tormodatt/tree-sitter-kotlin", version = "0.0.1" } 45 | tree-sitter-latex = { git = "https://github.com/latex-lsp/tree-sitter-latex", version = "0.0.1" } 46 | tree-sitter-lua = { git = "https://github.com/nvim-treesitter/tree-sitter-lua", version = "0.0.1" } 47 | tree-sitter-markdown = "0.7.1" 48 | # tree-sitter-nix = { git = "https://github.com/cstrahan/tree-sitter-nix", version = "0.0.1" } 49 | tree-sitter-ocaml = "0.19.0" 50 | # tree-sitter-ocamllex = { git = "https://github.com/atom-ocaml/tree-sitter-ocamllex", version = "*" } 51 | # tree-sitter-php = { git = "https://github.com/tree-sitter/tree-sitter-php", version = "0.19.0" } 52 | tree-sitter-python = "0.19.0" 53 | tree-sitter-query = { git = "https://github.com/nvim-treesitter/tree-sitter-query", version = "0.0.1" } 54 | # tree-sitter-r = { git = "https://github.com/r-lib/tree-sitter-r", version = "*" } 55 | # tree-sitter-rst = { git = "https://github.com/stsewd/tree-sitter-rst", version = "0.0.1" } 56 | tree-sitter-ruby = { git = "https://github.com/tree-sitter/tree-sitter-ruby", version = "0.19.0" } 57 | tree-sitter-rust = "0.19.0" 58 | tree-sitter-scala = { git = "https://github.com/tree-sitter/tree-sitter-scala", version = "0.19.0" } 59 | # tree-sitter-scss = { git = "https://github.com/elianiva/tree-sitter-scss", version = "0.0.1" } 60 | # tree-sitter-supercollider = { git = "https://github.com/madskjeldgaard/tree-sitter-supercollider", version = "*" } 61 | tree-sitter-svelte = "0.8.1" 62 | # tree-sitter-swift = { git = "https://github.com/tree-sitter/tree-sitter-swift", version = "*" } 63 | # tree-sitter-teal = { git = "https://github.com/euclidianAce/tree-sitter-teal", version = "0.0.1" } 64 | # tree-sitter-toml = { git = "https://github.com/ikatyang/tree-sitter-toml", version = "*" } 65 | tree-sitter-typescript = "0.19.0" 66 | # tree-sitter-verilog = { git = "https://github.com/tree-sitter/tree-sitter-verilog", version = "0.0.1" } 67 | # tree-sitter-vue = { git = "https://github.com/ikatyang/tree-sitter-vue", version = "*" } 68 | # tree-sitter-yaml = { git = "https://github.com/ikatyang/tree-sitter-yaml", version = "*" } 69 | # tree-sitter-zig = { git = "https://github.com/Himujjal/tree-sitter-zig", version = "0.0.1" } 70 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Adam P. Regasz-Rethy 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 | -------------------------------------------------------------------------------- /QUERIES.md: -------------------------------------------------------------------------------- 1 | # Queries 2 | 3 | `tcount` has the ability to count [Tree-sitter queries (skim this first)](https://tree-sitter.github.io/tree-sitter/using-parsers#pattern-matching-with-queries). These allow you to match patterns in the syntax tree for more fine grained counting. 4 | 5 | **Note**: Simple queries can probably be replaced with --kind-pattern. (e.g. `--kind-pattern=".*comment"` usually suffices to count comments.) 6 | 7 | To count a query named `foo`, pass the option `--query=foo`. If your query `foo` has capture groups `bar` and `baz` then these capture groups can instead be counted with `--query=foo@bar,baz`. 8 | 9 | ## Query Directories 10 | 11 | All queries are user-defined inside query directories. A query directory is a directory that contains subdirectories named for a specific language which themselves contain query files named `{query}.scm` files for each query. For example, a query directory could have a structure similar to: 12 | 13 | ``` 14 | query_directory/ 15 | ├── rust/ 16 | │   ├── functions_query.scm 17 | │   └── keywords_query.scm 18 | ├── ruby/ 19 | │   ├── functions_query.scm 20 | │   └── keywords_query.scm 21 | └── go 22 |    └── functions_query.scm 23 | ``` 24 | 25 | In the above query directory, we have a two queries `functions_query` (supports Rust, Ruby, and Go) and `keywords_query` (supports Rust and Ruby). If a language doesn't have a specific query file, then it will be counted as 0. 26 | 27 | The language directories must be named according to third column in the output from `--list-languages` (e.g. `C#` queries go under `c_sharp`). 28 | 29 | ### Query Directory Locations 30 | 31 | When `--query=foo` is used, `tcount` will begin looking for the `foo` query inside a query directory. When it find a `foo` query for any language, it will stop looking for other query directories and only languages with `foo.scm` in the current query directory will have the query counted. 32 | 33 | When looking for a query directory with the query, the following locations will be searched: 34 | 35 | 1. `$PWD/.tcount_queries/` will be considered a query directory 36 | 2. `$XDG_CONFIG_HOME/tcount/*/` will each (post-expansion) be considered query directories 37 | 38 | There is no guarantee about the expansion order for #2 so conflicting queries results in undefined behaviour as to which is used. 39 | 40 | ## Writing your own queries 41 | 42 | The most important resource are the [Tree-sitter Query Docs](https://tree-sitter.github.io/tree-sitter/using-parsers#pattern-matching-with-queries). 43 | 44 | [nvim-treesitter](https://github.com/nvim-treesitter/nvim-treesitter/tree/master/queries) has a lot of queries that can be copy pasted. 45 | 46 | Most parsers generated using Tree-sitter have a `queries/` directory which have queries that can be copy pasted. For example, [tree-sitter-go](https://github.com/tree-sitter/tree-sitter-go/tree/master/queries). 47 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | tcount 3 |

4 | 5 |

(pronounced "tee-count")

6 | 7 |

Count your code by tokens, types of syntax tree nodes, and patterns in the syntax tree.

8 | 9 | [![Build Status](https://github.com/RRethy/tcount/actions/workflows/rust.yml/badge.svg)](https://github.com/RRethy/tcount/actions) 10 | 11 | **Note: A dependency broke the release build, execution only works via `cargo run` and `cargo build` atm.** 12 | 13 | ## Installation 14 | 15 | ```bash 16 | git clone https://github.com/RRethy/tcount.git 17 | cd tcount 18 | cargo build --release 19 | cp ./target/release/tcount ~/bin 20 | PATH=~/bin:$PATH 21 | ``` 22 | 23 | # Quick Start 24 | 25 | Simply run `tcount` in your project root to count tokens and files and print the results grouped by Language. E.g., 26 | 27 | ```bash 28 | tcount 29 | ``` 30 | ```txt 31 | ──────────────────────────── 32 | Group Files Tokens 33 | ──────────────────────────── 34 | Rust 18 10309 35 | Go 8 4539 36 | Ruby 6 1301 37 | ──────────────────────────── 38 | ``` 39 | 40 | ## Requirements 41 | 42 | - Lastest stable [Rust](https://www.rust-lang.org/) compiler. 43 | - Mac or Linux (untested on Windows but most functionality should work, `--query` option likely will not work on Windows) 44 | 45 | # tcount Cookbook 46 | 47 | **Note**: None of these use --query, see [Queries](https://github.com/RRethy/tcount/blob/master/QUERIES.md) for information on that option 48 | 49 |
Compare size of each language in pwd 50 |

51 | 52 | ```bash 53 | tcount 54 | ``` 55 | ```txt 56 | ──────────────────────────── 57 | Group Files Tokens 58 | ──────────────────────────── 59 | Rust 18 10309 60 | Go 8 4539 61 | Ruby 6 1301 62 | ──────────────────────────── 63 | ``` 64 | 65 |

66 |
67 | 68 |
Top 5 files by token count 69 |

70 | 71 | ```bash 72 | tcount --groupby=file --top=5 73 | ``` 74 | ```txt 75 | ────────────────────────────────── 76 | Group Files Tokens 77 | ────────────────────────────────── 78 | ./src/count.rs 1 2451 79 | ./src/language.rs 1 1685 80 | ./src/main.rs 1 1214 81 | ./src/output.rs 1 1157 82 | ./src/cli.rs 1 757 83 | ────────────────────────────────── 84 | ``` 85 | 86 |

87 |
88 | 89 |
Compare size of two directories 90 |

91 | 92 | ```bash 93 | tcount --groupby=arg go/scc/ rust/tokei/ 94 | ``` 95 | ```txt 96 | ───────────────────────────────────────────── 97 | Group Files Tokens 98 | ───────────────────────────────────────────── 99 | go/scc 170 479544 100 | rust/tokei 152 39797 101 | ───────────────────────────────────────────── 102 | ``` 103 | 104 |

105 |
106 | 107 |
Compare size of a Go file and a Rust file 108 |

109 | 110 | ```bash 111 | tcount --groupby=file foo.go foo.rs 112 | ``` 113 | ```txt 114 | ──────────────────────────── 115 | Group Files Tokens 116 | ──────────────────────────── 117 | foo.rs 1 1214 118 | foo.go 1 757 119 | ──────────────────────────── 120 | ``` 121 | 122 |

123 |
124 | 125 |
Count comments for each language 126 |

127 | 128 | ```bash 129 | tcount --kind-pattern=".*comment" 130 | ``` 131 | ```txt 132 | ────────────────────────────────────────────────── 133 | Group Files Tokens Pattern(.*comment) 134 | ────────────────────────────────────────────────── 135 | Rust 18 10309 78 136 | Go 7 1302 35 137 | Ruby 4 802 12 138 | ────────────────────────────────────────────────── 139 | ``` 140 | 141 | **Note**: Comment nodes can have different names depending on the parser. For a language, you can look in the node-types.json file in the parser repo to see what names are given to different nodes (e.g. [Go Parser Repo's node-types.json](https://github.com/tree-sitter/tree-sitter-go/blob/master/src/node-types.json)) 142 | 143 |

144 |
145 | 146 |
Track change in project size over time 147 |

148 | 149 | ```bash 150 | tcount --format=csv > tcount-$(date +%m-%d-%Y).csv 151 | ``` 152 | 153 | These CSV files can then be read and graphed using your tool of choice. 154 | 155 |

156 |
157 | 158 |
Compare size of all Go files vs all Rust files in foo/ 159 |

160 | 161 | ```bash 162 | tcount --whitelist Go Rust -- foo/ 163 | ``` 164 | ```txt 165 | ────────────────────── 166 | Group Files Tokens 167 | ────────────────────── 168 | Rust 9 9034 169 | Go 6 2011 170 | ────────────────────── 171 | ``` 172 | 173 |

174 |
175 | 176 |
Supported languages 177 |

178 | 179 | ```bash 180 | tcount --list-languages 181 | ``` 182 | ``` 183 | ────────────────────────────────────────────────────────────────────── 184 | Language Extensions Query Dir Name 185 | ────────────────────────────────────────────────────────────────────── 186 | Bash .bash bash 187 | BibTeX .bib bibtex 188 | C .h,.c c 189 | C# .csx,.cs c_sharp 190 | Clojure .clj clojure 191 | C++ .cxx,.c++,.h++,.hh,.cc,.cpp,.hpp cpp 192 | CSS .css css 193 | Elm .elm elm 194 | Erlang .erl,.hrl erlang 195 | Go .go go 196 | HTML .html html 197 | Java .java java 198 | Javascript .js,.mjs javascript 199 | JSON .json json 200 | Julia .jl julia 201 | LaTeX .tex latex 202 | Markdown .md markdown 203 | OCaml .ml ocaml 204 | OCaml Interface .mli ocaml_interface 205 | Python .pyw,.py python 206 | Tree-sitter Query .scm query 207 | Ruby .rb ruby 208 | Rust .rs rust 209 | Scala .scala,.sc scala 210 | Svelte .svelte svelte 211 | Typescript .ts typescript 212 | ────────────────────────────────────────────────────────────────────── 213 | ``` 214 | 215 |

216 |
217 | 218 | # Why count tokens instead of lines 219 | 220 | 1. Counting lines rewards dense programs. For example, 221 | 222 | ```c 223 | int nums[4] = { 1, 2, 3, 4 }; 224 | int mult[4] = {0}; 225 | for (int i = 0; i < 4; i++) { 226 | mult[i] = nums[i] * 2; 227 | } 228 | printf("[%d] [%d] [%d] [%d]", mult[0], mult[1], mult[2], mult[3]); 229 | ``` 230 | 231 | ```go 232 | nums := []int{1, 2, 3, 4} 233 | mult := make([]int, 4) 234 | for i, n := range nums { 235 | mult[i] = n * 2 236 | } 237 | fmt.Println(mult) 238 | ``` 239 | 240 | Are these programs the same size? They are each 6 lines long, but clearly the Go version is considerably smaller than the C version. While this is a contrived example, line counting still rewards dense programs and dense programming languages. 241 | 242 | 2. Counting lines rewards short variable names. Is `ns` shorter than `namespace`? By bytes it is, when used throughout a project it likely will result in fewer line breaks, but I don't think a program should be considered *smaller* just because it uses cryptic variable names whenever possible. 243 | 244 | 3. Counting lines penalizes line comments mixed with code. Consider the following contrived example, 245 | 246 | ```rust 247 | v.iter() // iterate over the vector 248 | .map(|n| n * 2) // multiply each number by two 249 | .collect::Vec(); // collect the iterator into a vector of u32 250 | ``` 251 | 252 | Without the comments, it could be written as `v.iter().map(|n| n * 2).collect::Vec<32>();`. 253 | 254 | 4. Short syntactical elements in languages are rewarded. For example: 255 | 256 | ```ruby 257 | [1, 2, 3, 4].map { |n| n * 2 } 258 | ``` 259 | 260 | Compared with the equivalent 261 | 262 | ```ruby 263 | [1, 2, 3, 4].map do |n| 264 | n * 2 265 | end 266 | ``` 267 | 268 | 5. Counting lines rewards horizontal programming and penalizes vertical programming 269 | 270 | # Usage 271 | 272 | ```bash 273 | tcount -h 274 | ``` 275 | ``` 276 | tcount 0.1.0 277 | Count your code by tokens, node kinds, and patterns in the syntax tree. 278 | 279 | USAGE: 280 | tcount [FLAGS] [OPTIONS] [--] [paths]... 281 | 282 | FLAGS: 283 | --count-hidden Count hidden files 284 | -h, --help Prints help information 285 | --list-languages Show a list of supported languages for parsing 286 | --no-dot-ignore Don't respect .ignore files 287 | --no-git Don't respect gitignore and .git/info/exclude files 288 | --no-parent-ignore Don't respect ignore files from parent directories 289 | --show-totals Show column totals. This is not affected by --top 290 | -V, --version Prints version information 291 | 292 | OPTIONS: 293 | --blacklist ... Blacklist of languages not to parse. This is overriden by --whitelist and 294 | must be an exact match 295 | --format One of table|csv [default: table] 296 | --groupby One of language|file|arg. "arg" will group by the `paths` arguments provided 297 | [default: language] 298 | -k, --kind ... kinds of nodes in the syntax tree to count. See node-types.json in the 299 | parser's repo to see the names of nodes or use https://tree- 300 | sitter.github.io/tree-sitter/playground. 301 | -p, --kind-pattern ... Patterns of node kinds to count in the syntax tree (e.g. ".*comment" to 302 | match nodes of type "line_comment", "block_comment", and "comment"). 303 | Supports Rust regular expressions 304 | --query ... Tree-sitter queries to match and count. Captures can also be counted with 305 | --query=query_name@capture_name,capture_name2. See 306 | https://github.com/RRethy/tcount/blob/master/QUERIES.md for more information 307 | --sort-by One of group|numfiles|tokens. "group" will sort based on --groupby value 308 | [default: tokens] 309 | --top How many of the top results to show 310 | --verbose Logging level. 0 to not print errors. 1 to print IO and filesystem errors. 2 311 | to print parsing errors. 3 to print everything else. [default: 0] 312 | --whitelist ... Whitelist of languages to parse. This overrides --blacklist and must be an 313 | exact match 314 | 315 | ARGS: 316 | ... Files and directories to parse and count. [default: .] 317 | ``` 318 | 319 | # Counting Tree-sitter Queries 320 | 321 | See [QUERIES.md](https://github.com/RRethy/tcount/blob/master/QUERIES.md) 322 | 323 | # Performance 324 | 325 | `tcount` parses each file using a Tree-sitter parser to create a full syntax tree. This takes more time than only counting lines of code/comments so programs like [tokei](https://github.com/XAMPPRocky/tokei), [scc](https://github.com/boyter/scc), and [cloc](https://github.com/AlDanial/cloc) will typically be faster than `tcount`. 326 | 327 | Here are some benchmarks using [hyperfine](https://github.com/sharkdp/hyperfine) to give an overview of how much slower it is than line counting programs: 328 | 329 | [**tcount**](https://github.com/RRethy/tcount) 330 | 331 | | Program | Runtime | 332 | |---------|------------------| 333 | | tcount | 19.5 ms ± 1.7 ms | 334 | | scc | 13.0 ms ± 1.4 ms | 335 | | tokei | 7.2 ms ± 1.2 ms | 336 | | cloc | 1.218 s ± 0.011 s | 337 | 338 | [**Redis**](https://github.com/redis/redis) 339 | 340 | | Program | Runtime | 341 | |---------|------------------| 342 | | tcount | 1.339 s ± 0.125 s | 343 | | scc | 49.9 ms ± 1.6 ms | 344 | | tokei | 79.9 ms ± 5.3 ms | 345 | | cloc | 1.331 s ± 0.016 s | 346 | 347 | [**CPython**](https://github.com/python/cpython) 348 | 349 | | Program | Runtime | 350 | |---------|------------------| 351 | | tcount | 11.580 s ± 0.199 s | 352 | | scc | 256.7 ms ± 3.0 ms | 353 | | tokei | 512.2 ms ± 96.4 ms | 354 | | cloc | 12.467 s ± 0.139 s | 355 | 356 | # Limitations 357 | 358 | - `tcount` does not support nested languages like ERB. This may change in the future. 359 | - It's not always clear what is a token, `tcount` treats any node in the syntax tree without children as a token. This usually works, but in some cases, like strings in the Rust Tree-sitter parser which can have children (escape codes), it may produce slightly expected results. 360 | 361 | # Why Tree-sitter 362 | 363 | Tree-sitter has relatively efficient parsing and has support for many languages without the need to create and maintain individual parsers or lexers. Support for new languages is easy and only requires and up-to-date Tree-sitter parser. 364 | 365 | # Contributing 366 | 367 | To add support for a new language, add it's information to `https://github.com/RRethy/tcount/blob/master/src/language.rs` and add the language's Tree-sitter parser crate to `Cargo.toml`. 368 | 369 | # Acknowledgements 370 | 371 | All parsing is done using [Tree-sitter](https://tree-sitter.github.io/tree-sitter) parsers 372 | -------------------------------------------------------------------------------- /src/cli.rs: -------------------------------------------------------------------------------- 1 | use crate::output::Format; 2 | use crate::query::Query; 3 | use regex::Regex; 4 | use std::format; 5 | use std::path::PathBuf; 6 | use std::str::FromStr; 7 | use structopt::StructOpt; 8 | 9 | #[derive(StructOpt, Debug)] 10 | #[structopt( 11 | name = "tcount", 12 | about = "Count your code by tokens, node kinds, and patterns in the syntax tree." 13 | )] 14 | pub struct Cli { 15 | #[structopt( 16 | long, 17 | help = "Logging level. 0 to not print errors. 1 to print IO and filesystem errors. 2 to print parsing errors. 3 to print everything else.", 18 | default_value = "0" 19 | )] 20 | pub verbose: u8, 21 | 22 | #[structopt( 23 | short, 24 | long, 25 | help = "kinds of nodes in the syntax tree to count. See node-types.json in the parser's repo to see the names of nodes or use https://tree-sitter.github.io/tree-sitter/playground." 26 | )] 27 | pub kind: Vec, 28 | 29 | #[structopt( 30 | short = "p", 31 | long, 32 | help = "Patterns of node kinds to count in the syntax tree (e.g. \".*comment\" to match nodes of type \"line_comment\", \"block_comment\", and \"comment\"). Supports Rust regular expressions" 33 | )] 34 | pub kind_pattern: Vec, 35 | 36 | #[structopt( 37 | long, 38 | help = "Tree-sitter queries to match and count. Captures can also be counted with --query=query_name@capture_name,capture_name2. See https://github.com/RRethy/tcount/blob/master/QUERIES.md for more information" 39 | )] 40 | pub query: Vec, 41 | 42 | #[structopt( 43 | long, 44 | default_value = "tokens", 45 | help = "One of group|numfiles|tokens. \"group\" will sort based on --groupby value" 46 | )] 47 | pub sort_by: SortBy, 48 | 49 | #[structopt( 50 | long, 51 | default_value = "language", 52 | help = "One of language|file|arg. \"arg\" will group by the `paths` arguments provided" 53 | )] 54 | pub groupby: GroupBy, 55 | 56 | #[structopt(long, default_value = "table", help = "One of table|csv")] 57 | pub format: Format, 58 | 59 | #[structopt(long, help = "Don't respect gitignore and .git/info/exclude files")] 60 | pub no_git: bool, 61 | 62 | #[structopt(long, help = "Don't respect .ignore files")] 63 | pub no_dot_ignore: bool, 64 | 65 | #[structopt(long, help = "Don't respect ignore files from parent directories")] 66 | pub no_parent_ignore: bool, 67 | 68 | #[structopt(long, help = "Count hidden files")] 69 | pub count_hidden: bool, 70 | 71 | #[structopt( 72 | long, 73 | help = "Whitelist of languages to parse. This overrides --blacklist and must be an exact match" 74 | )] 75 | pub whitelist: Vec, 76 | 77 | #[structopt( 78 | long, 79 | help = "Blacklist of languages not to parse. This is overriden by --whitelist and must be an exact match" 80 | )] 81 | pub blacklist: Vec, 82 | 83 | #[structopt(long, help = "Show a list of supported languages for parsing")] 84 | pub list_languages: bool, 85 | 86 | #[structopt(long, help = "Show column totals. This is not affected by --top")] 87 | pub show_totals: bool, 88 | 89 | #[structopt(long, help = "How many of the top results to show")] 90 | pub top: Option, 91 | 92 | #[structopt( 93 | default_value = ".", 94 | help = "Files and directories to parse and count." 95 | )] 96 | pub paths: Vec, 97 | } 98 | 99 | #[derive(Debug, PartialEq, Eq)] 100 | pub enum SortBy { 101 | Group, 102 | NumFiles, 103 | Tokens, 104 | } 105 | 106 | impl FromStr for SortBy { 107 | type Err = String; 108 | 109 | fn from_str(s: &str) -> Result { 110 | match s { 111 | "group" => Ok(SortBy::Group), 112 | "numfiles" => Ok(SortBy::NumFiles), 113 | "tokens" => Ok(SortBy::Tokens), 114 | _ => Err(format!( 115 | "\"{}\" is not a supported argument to --sort-by. Use one of group|numfiles|tokens", 116 | s 117 | )), 118 | } 119 | } 120 | } 121 | 122 | #[derive(Debug, PartialEq, Eq)] 123 | pub enum GroupBy { 124 | Language, 125 | File, 126 | Arg, 127 | } 128 | 129 | impl FromStr for GroupBy { 130 | type Err = String; 131 | 132 | fn from_str(s: &str) -> Result { 133 | match s { 134 | "language" => Ok(GroupBy::Language), 135 | "file" => Ok(GroupBy::File), 136 | "arg" => Ok(GroupBy::Arg), 137 | _ => Err(format!( 138 | "\"{}\" is not a supported argument to --groupby. Use one of language|file", 139 | s 140 | )), 141 | } 142 | } 143 | } 144 | 145 | #[cfg(test)] 146 | mod tests { 147 | use super::*; 148 | 149 | #[test] 150 | fn group_by_from_str() { 151 | assert_eq!(GroupBy::Language, GroupBy::from_str("language").unwrap()); 152 | assert_eq!(GroupBy::File, GroupBy::from_str("file").unwrap()); 153 | assert_eq!(GroupBy::Arg, GroupBy::from_str("arg").unwrap()); 154 | } 155 | 156 | #[test] 157 | fn sort_by_from_str() { 158 | assert_eq!(SortBy::Group, SortBy::from_str("group").unwrap()); 159 | assert_eq!(SortBy::NumFiles, SortBy::from_str("numfiles").unwrap()); 160 | assert_eq!(SortBy::Tokens, SortBy::from_str("tokens").unwrap()); 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /src/count.rs: -------------------------------------------------------------------------------- 1 | use crate::error::{Error, Result}; 2 | use crate::language::Language; 3 | use crate::query::{Query, QueryKind}; 4 | use crate::tree::TreeIterator; 5 | use regex::Regex; 6 | use std::collections::HashMap; 7 | use std::fs; 8 | use std::ops::AddAssign; 9 | use std::path::Path; 10 | use tree_sitter::{Node, Parser, QueryCursor}; 11 | 12 | /// Counts contains the cumulative totals for the how many files, number of tokens, number of nodes 13 | /// matching each kind specified by --kind, and number of matches for each query specified by 14 | /// --query. @nqueries is ordered first by the queries arguments and then by captures. 15 | #[derive(Debug, Eq, PartialEq, Clone)] 16 | pub struct Counts { 17 | pub nfiles: u64, 18 | pub ntokens: u64, 19 | pub nkinds: Vec, 20 | pub nkind_patterns: Vec, 21 | pub nqueries: Vec, 22 | } 23 | 24 | impl Counts { 25 | /// Create a Counts struct with zero values and correctly sized fields 26 | pub fn empty(nkinds: usize, nkind_patterns: usize, queries: &[Query]) -> Counts { 27 | Counts { 28 | nfiles: 0, 29 | ntokens: 0, 30 | nkinds: vec![0; nkinds], 31 | nkind_patterns: vec![0; nkind_patterns], 32 | nqueries: Self::nqueries(queries, HashMap::new(), HashMap::new()), 33 | } 34 | } 35 | 36 | /// Convert @nmatches and @ncaptures into a deterministically ordered vector 37 | fn nqueries( 38 | queries: &[Query], 39 | nmatches: HashMap<&String, u64>, 40 | ncaptures: HashMap<(&String, &String), u64>, 41 | ) -> Vec { 42 | queries 43 | .iter() 44 | .flat_map(|query| match query.kind { 45 | QueryKind::Match => { 46 | vec![nmatches.get(&query.name).unwrap_or(&0)] 47 | } 48 | QueryKind::Captures(ref names) => names 49 | .iter() 50 | .map(|name| ncaptures.get(&(&query.name, name)).unwrap_or(&0)) 51 | .collect(), 52 | }) 53 | .copied() 54 | .collect() 55 | } 56 | } 57 | 58 | impl AddAssign for Counts { 59 | fn add_assign(&mut self, other: Self) { 60 | #[inline(always)] 61 | fn add(l: &mut [u64], r: &Vec) { 62 | // element-wise addition of two equal-sized vectors 63 | l.iter_mut().zip(r).for_each(|(a, b)| *a += b); 64 | } 65 | self.nfiles += other.nfiles; 66 | self.ntokens += other.ntokens; 67 | add(&mut self.nkinds, &other.nkinds); 68 | add(&mut self.nkind_patterns, &other.nkind_patterns); 69 | add(&mut self.nqueries, &other.nqueries); 70 | } 71 | } 72 | 73 | impl Counts { 74 | /// Try to count @path for the specified arguments 75 | pub fn from_path( 76 | path: impl AsRef, 77 | lang: &Language, 78 | kinds: &Vec, 79 | kind_patterns: &Vec, 80 | queries: &[Query], 81 | ) -> Result { 82 | let ts_lang = { 83 | match lang.get_treesitter_language() { 84 | Ok(ts_lang) => ts_lang, 85 | Err(_) => { 86 | // Unsupported language gets an *empty* Counts struct 87 | return Ok(Counts { 88 | nfiles: 1, 89 | ..Counts::empty(kinds.len(), kind_patterns.len(), queries) 90 | }); 91 | } 92 | } 93 | }; 94 | 95 | let mut ntokens = 0; 96 | let mut nkinds = vec![0; kinds.len()]; 97 | let mut nkind_patterns = vec![0; kind_patterns.len()]; 98 | let mut nmatch_queries = HashMap::new(); 99 | let mut ncapture_queries = HashMap::new(); 100 | 101 | let text = fs::read_to_string(path.as_ref())?; 102 | let mut parser = Parser::new(); 103 | parser 104 | .set_language(ts_lang) 105 | .expect("Unexpected internal error setting parser language"); 106 | 107 | let mut qcursor = QueryCursor::new(); 108 | let text_callback = |n: Node| &text[n.byte_range()]; // weird but needed argument for queries 109 | match parser.parse(&text, None) { 110 | Some(tree) => { 111 | queries.iter().for_each(|query| { 112 | if let Some(ts_query) = query.langs.get(lang) { 113 | match &query.kind { 114 | QueryKind::Match => { 115 | nmatch_queries.insert( 116 | &query.name, 117 | qcursor 118 | .matches(ts_query, tree.root_node(), text_callback) 119 | .count() as u64, 120 | ); 121 | } 122 | QueryKind::Captures(_) => { 123 | // We should only be finding capture names that were provided as 124 | // arguments since other capture names have been disabled in the 125 | // query 126 | let capture_names = ts_query.capture_names(); 127 | qcursor 128 | .captures(ts_query, tree.root_node(), text_callback) 129 | .for_each(|(qmatch, _)| { 130 | qmatch.captures.iter().for_each(|capture| { 131 | *ncapture_queries 132 | .entry(( 133 | &query.name, 134 | &capture_names[capture.index as usize], 135 | )) 136 | .or_insert(0) += 1; 137 | }); 138 | }); 139 | } 140 | } 141 | } 142 | }); 143 | 144 | TreeIterator::new(&tree).for_each(|node| { 145 | if !node.is_missing() { 146 | // count each terminal node which is the closest we can get to counting 147 | // tokens. For some tokens this is a bit misleading since they can have 148 | // children (e.g. string_literal in rust), but it's the closest we can 149 | // achieve with tree-sitter. 150 | if node.child_count() == 0 && !node.is_extra() && node.parent().is_some() { 151 | ntokens += 1; 152 | } 153 | 154 | // count each --kinds that match the current nodes kind 155 | kinds.iter().enumerate().for_each(|(i, kind)| { 156 | if kind == node.kind() { 157 | nkinds[i] += 1; 158 | } 159 | }); 160 | 161 | // count each --kind_patterns that match the current nodes kind 162 | kind_patterns.iter().enumerate().for_each(|(i, kind)| { 163 | if kind.is_match(node.kind()) { 164 | nkind_patterns[i] += 1; 165 | } 166 | }); 167 | } 168 | }); 169 | let nqueries = Counts::nqueries(queries, nmatch_queries, ncapture_queries); 170 | Ok(Counts { 171 | nfiles: 1, 172 | ntokens, 173 | nkinds, 174 | nkind_patterns, 175 | nqueries, 176 | }) 177 | } 178 | None => Err(Error::Parser(path.as_ref().to_path_buf())), 179 | } 180 | } 181 | } 182 | 183 | #[cfg(test)] 184 | mod tests { 185 | use super::*; 186 | use std::str::FromStr; 187 | 188 | fn queries_with_captures() -> Vec { 189 | vec![ 190 | Query::from_str("comment").unwrap(), 191 | Query::from_str("keyword@ifelse,repeat").unwrap(), 192 | Query::from_str("string_literal").unwrap(), 193 | ] 194 | } 195 | 196 | fn queries() -> Vec { 197 | vec![ 198 | Query::from_str("comment").unwrap(), 199 | Query::from_str("string_literal").unwrap(), 200 | ] 201 | } 202 | 203 | #[test] 204 | fn counting_unsupported_language() { 205 | let queries = Vec::new(); 206 | let got = Counts::from_path( 207 | "tests/fixtures/unsupported.abc", 208 | &Language::Unsupported, 209 | &Vec::new(), 210 | &Vec::new(), 211 | &queries, 212 | ); 213 | let expected = Counts { 214 | nfiles: 1, 215 | ntokens: 0, 216 | nkinds: Vec::new(), 217 | nkind_patterns: Vec::new(), 218 | nqueries: Vec::new(), 219 | }; 220 | assert_eq!(expected, got.unwrap()); 221 | } 222 | 223 | #[test] 224 | fn counting_nothing() { 225 | let queries = queries(); 226 | let got = Counts::from_path( 227 | "tests/fixtures/empty.rs", 228 | &Language::Rust, 229 | &Vec::new(), 230 | &Vec::new(), 231 | &queries, 232 | ); 233 | let expected = Counts { 234 | nfiles: 1, 235 | ntokens: 0, 236 | nkinds: Vec::new(), 237 | nkind_patterns: Vec::new(), 238 | nqueries: vec![0, 0], 239 | }; 240 | assert_eq!(expected, got.unwrap()); 241 | } 242 | 243 | #[test] 244 | fn counting_tokens() { 245 | let queries = Vec::new(); 246 | let got = Counts::from_path( 247 | "tests/fixtures/rust1.rs", 248 | &Language::Rust, 249 | &Vec::new(), 250 | &Vec::new(), 251 | &queries, 252 | ); 253 | let expected = Counts { 254 | nfiles: 1, 255 | ntokens: 33, 256 | nkinds: Vec::new(), 257 | nkind_patterns: Vec::new(), 258 | nqueries: Vec::new(), 259 | }; 260 | assert_eq!(expected, got.unwrap()); 261 | } 262 | 263 | #[test] 264 | fn counting_tokens_for_invalid_syntax() { 265 | let queries = Vec::new(); 266 | let got = Counts::from_path( 267 | "tests/fixtures/invalid.rs", 268 | &Language::Rust, 269 | &Vec::new(), 270 | &Vec::new(), 271 | &queries, 272 | ); 273 | let expected = Counts { 274 | nfiles: 1, 275 | ntokens: 30, 276 | nkinds: Vec::new(), 277 | nkind_patterns: Vec::new(), 278 | nqueries: Vec::new(), 279 | }; 280 | assert_eq!(expected, got.unwrap()); 281 | } 282 | 283 | #[test] 284 | fn counting_node_kinds() { 285 | let queries = Vec::new(); 286 | let got = Counts::from_path( 287 | "tests/fixtures/rust1.rs", 288 | &Language::Rust, 289 | &vec!["identifier".into(), "::".into()], 290 | &Vec::new(), 291 | &queries, 292 | ); 293 | let expected = Counts { 294 | nfiles: 1, 295 | ntokens: 33, 296 | nkinds: vec![8, 3], 297 | nkind_patterns: Vec::new(), 298 | nqueries: Vec::new(), 299 | }; 300 | assert_eq!(expected, got.unwrap()); 301 | } 302 | 303 | #[test] 304 | fn counting_node_kind_patterns() { 305 | let queries = Vec::new(); 306 | let got = Counts::from_path( 307 | "tests/fixtures/rust1.rs", 308 | &Language::Rust, 309 | &vec!["block_comment".into(), "line_comment".into()], 310 | &vec![Regex::new(".*comment").unwrap()], 311 | &queries, 312 | ); 313 | let expected = Counts { 314 | nfiles: 1, 315 | ntokens: 33, 316 | nkinds: vec![1, 3], 317 | nkind_patterns: vec![4], 318 | nqueries: Vec::new(), 319 | }; 320 | assert_eq!(expected, got.unwrap()); 321 | } 322 | 323 | #[test] 324 | fn counting_queries() { 325 | let queries = queries(); 326 | let got = Counts::from_path( 327 | "tests/fixtures/rust1.rs", 328 | &Language::Rust, 329 | &Vec::new(), 330 | &Vec::new(), 331 | &queries, 332 | ); 333 | let expected = Counts { 334 | nfiles: 1, 335 | ntokens: 33, 336 | nkinds: Vec::new(), 337 | nkind_patterns: Vec::new(), 338 | nqueries: vec![4, 2], 339 | }; 340 | assert_eq!(expected, got.unwrap()); 341 | } 342 | 343 | #[test] 344 | fn counting_everything() { 345 | let queries = queries(); 346 | let got = Counts::from_path( 347 | "tests/fixtures/rust1.rs", 348 | &Language::Rust, 349 | &vec!["block_comment".into(), "line_comment".into()], 350 | &vec![Regex::new(".*comment").unwrap()], 351 | &queries, 352 | ); 353 | let expected = Counts { 354 | nfiles: 1, 355 | ntokens: 33, 356 | nkinds: vec![1, 3], 357 | nkind_patterns: vec![4], 358 | nqueries: vec![4, 2], 359 | }; 360 | assert_eq!(expected, got.unwrap()); 361 | } 362 | 363 | #[test] 364 | fn counts_vec_from_query_hashmap_counts() { 365 | let queries = queries_with_captures(); 366 | let mut nmatches = HashMap::new(); 367 | let comment = String::from("comment"); 368 | let string_literal = String::from("string_literal"); 369 | let keyword = String::from("keyword"); 370 | let ifelse = String::from("ifelse"); 371 | nmatches.insert(&comment, 5); 372 | nmatches.insert(&string_literal, 7); 373 | let mut ncaptures = HashMap::new(); 374 | ncaptures.insert((&keyword, &ifelse), 3); 375 | let got = Counts::nqueries(&queries, nmatches, ncaptures); 376 | assert_eq!(vec![5, 3, 0, 7], got); 377 | } 378 | 379 | #[test] 380 | fn counting_queries_with_captures() { 381 | let queries = queries_with_captures(); 382 | let got = Counts::from_path( 383 | "tests/fixtures/rust3.rs", 384 | &Language::Rust, 385 | &vec![], 386 | &vec![], 387 | &queries, 388 | ); 389 | let expected = Counts { 390 | nfiles: 1, 391 | ntokens: 73, 392 | nkinds: vec![], 393 | nkind_patterns: vec![], 394 | nqueries: vec![4, 4, 3, 2], 395 | }; 396 | assert_eq!(expected, got.unwrap()); 397 | } 398 | 399 | #[test] 400 | fn add_assign_counts() { 401 | let mut c1 = Counts { 402 | nfiles: 30, 403 | ntokens: 21, 404 | nkinds: vec![28, 28], 405 | nkind_patterns: vec![29, 20, 2], 406 | nqueries: vec![0, 44, 55], 407 | }; 408 | let c2 = Counts { 409 | nfiles: 19, 410 | ntokens: 31, 411 | nkinds: vec![5, 9], 412 | nkind_patterns: vec![6, 10, 14], 413 | nqueries: vec![33, 44], 414 | }; 415 | 416 | c1 += c2; 417 | let expected = Counts { 418 | nfiles: 49, 419 | ntokens: 52, 420 | nkinds: vec![33, 37], 421 | nkind_patterns: vec![35, 30, 16], 422 | nqueries: vec![33, 88, 55], 423 | }; 424 | assert_eq!(expected, c1); 425 | } 426 | } 427 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | use crate::language::Language; 2 | use glob::GlobError; 3 | use std::fmt::{self, Display}; 4 | use std::io; 5 | use std::path::PathBuf; 6 | use tree_sitter::QueryError; 7 | 8 | pub type Result = std::result::Result; 9 | 10 | #[derive(Debug)] 11 | pub enum Error { 12 | IO(io::Error), 13 | UnsupportedLanguage, 14 | Parser(PathBuf), 15 | QueryError(QueryError), 16 | Ignore(ignore::Error), 17 | LanguageIgnored(PathBuf, Language), 18 | Glob(GlobError), 19 | } 20 | 21 | impl Error { 22 | pub fn should_show(&self, verbose_lvl: u8) -> bool { 23 | match self { 24 | Error::IO(_) => verbose_lvl >= 1, 25 | Error::UnsupportedLanguage => verbose_lvl >= 3, 26 | Error::Parser(_) => verbose_lvl >= 2, 27 | Error::QueryError(_) => true, 28 | Error::Ignore(_) => verbose_lvl >= 1, 29 | Error::LanguageIgnored(_, _) => verbose_lvl >= 3, 30 | Error::Glob(_) => verbose_lvl >= 3, 31 | } 32 | } 33 | } 34 | 35 | impl Display for Error { 36 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 37 | match self { 38 | Error::IO(err) => writeln!(f, "IO Error: {}", err), 39 | Error::UnsupportedLanguage => writeln!(f, "Unsupported Language"), 40 | Error::Parser(path) => writeln!(f, "Parser Error for path {}", path.display()), 41 | Error::QueryError(err) => writeln!(f, "Tree-sitter Query Error: {:?}", err), 42 | Error::Ignore(err) => writeln!(f, "Error while walking filetree: {}", err), 43 | Error::LanguageIgnored(path, lang) => { 44 | writeln!( 45 | f, 46 | "Language({}) for path({}) is ignored due to whitelist/blacklist options", 47 | lang, 48 | path.display() 49 | ) 50 | } 51 | Error::Glob(err) => writeln!(f, "Error with globbing {}", err), 52 | } 53 | } 54 | } 55 | 56 | impl From for Error { 57 | fn from(err: io::Error) -> Error { 58 | Error::IO(err) 59 | } 60 | } 61 | 62 | impl From for Error { 63 | fn from(err: QueryError) -> Error { 64 | Error::QueryError(err) 65 | } 66 | } 67 | 68 | impl From for Error { 69 | fn from(err: ignore::Error) -> Error { 70 | Error::Ignore(err) 71 | } 72 | } 73 | 74 | impl From for Error { 75 | fn from(err: GlobError) -> Error { 76 | Error::Glob(err) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/fs.rs: -------------------------------------------------------------------------------- 1 | use crate::error::{Error, Result}; 2 | use rayon::prelude::*; 3 | use std::path::{Path, PathBuf}; 4 | 5 | /// Recursively iterate over @paths and produce a parallel and unordered iterator over the files encountered. 6 | pub fn iter_paths( 7 | paths: &[impl AsRef], 8 | no_git: bool, 9 | count_hidden: bool, 10 | no_dot_ignore: bool, 11 | no_parent_ignore: bool, 12 | ) -> impl ParallelIterator> { 13 | let mut builder = ignore::WalkBuilder::new(paths.first().unwrap()); 14 | let _ = &paths[1..].iter().for_each(|path| { 15 | builder.add(path); 16 | }); 17 | // We synchronously walk the filesystem and use rayon's .par_bridge to create a parallel 18 | // iterator over these results for processing. This is just as efficient (and sometimes more 19 | // so) as asynchronously walking the filesystem (with channels for inter-thread communication) 20 | // since the limiting factor is the parsing of each file, not the walking of the filesystem. 21 | builder 22 | .git_exclude(!no_git) 23 | .git_global(!no_git) 24 | .git_ignore(!no_git) 25 | .hidden(!count_hidden) 26 | .ignore(!no_dot_ignore) 27 | .parents(!no_parent_ignore) 28 | .build() 29 | .par_bridge() 30 | .filter_map(|entry| match entry { 31 | Ok(dir) => { 32 | if dir.file_type().map_or(false, |ft| ft.is_file()) { 33 | Some(Ok(dir.into_path())) 34 | } else { 35 | None 36 | } 37 | } 38 | Err(err) => Some(Err(Error::from(err))), 39 | }) 40 | } 41 | -------------------------------------------------------------------------------- /src/language.rs: -------------------------------------------------------------------------------- 1 | use crate::error::{Error, Result}; 2 | use crate::output::print_languages; 3 | use std::cmp::{Eq, Ord, PartialEq, PartialOrd}; 4 | use std::collections::HashMap; 5 | use std::ffi::OsString; 6 | use std::fmt; 7 | use std::path::Path; 8 | 9 | /// There are many commented out languages, these languages do not have up to date Tree-sitter 10 | /// parsers. To add support for them, the parser needs to be update to use tree-sitter v0.19.3 11 | /// 12 | /// List of languages that are matched. Some of the languages are not supported since they either 13 | /// don't currently have a tree-sitter parser, or the tree-sitter parser is out-of-date and depends 14 | /// on an old version of the tree-sitter crate (<0.19.3). 15 | #[derive(Clone, Debug, PartialOrd, PartialEq, Eq, Ord, Hash)] 16 | pub enum Language { 17 | Bash, 18 | BibTeX, 19 | C, 20 | CSharp, 21 | Clojure, 22 | Cpp, 23 | Css, 24 | Dart, 25 | Elm, 26 | Erlang, 27 | EmbeddedTemplate, 28 | Fennel, 29 | GDScript, 30 | Go, 31 | GraphQL, 32 | Haskell, 33 | Html, 34 | Java, 35 | Javascript, 36 | Json, 37 | Julia, 38 | Kotlin, 39 | LaTeX, 40 | Lua, 41 | Markdown, 42 | Nix, 43 | OCaml, 44 | OCamlInterface, 45 | OCamlLex, 46 | Php, 47 | Python, 48 | Query, 49 | R, 50 | Rst, 51 | Ruby, 52 | Rust, 53 | Scala, 54 | Scss, 55 | Supercollider, 56 | Svelte, 57 | Swift, 58 | Teal, 59 | Toml, 60 | Typescript, 61 | Tsx, 62 | Verilog, 63 | Vue, 64 | Yaml, 65 | Zig, 66 | Unsupported, 67 | } 68 | 69 | impl Language { 70 | /// print all the supported languages, their file extensions, and query directory names 71 | pub fn print_all() { 72 | let mut lang_exts: Vec<(&Language, Vec)> = EXT_TO_LANGUAGE 73 | .into_iter() 74 | .filter(|(_ext, lang)| lang.get_treesitter_language().is_ok()) 75 | .fold(HashMap::new(), |mut acc, (ext, lang)| { 76 | acc.entry(lang) 77 | .or_insert(Vec::new()) 78 | .push(format!(".{}", ext)); 79 | acc 80 | }) 81 | .into_iter() 82 | .collect(); 83 | lang_exts.sort_by(|(l1, _), (l2, _)| l1.cmp(l2)); 84 | let lang_dirs = DIR_TO_LANGUAGE 85 | .into_iter() 86 | .filter(|(_dir, lang)| lang.get_treesitter_language().is_ok()) 87 | .fold(HashMap::new(), |mut acc, (dir, lang)| { 88 | acc.entry(lang).or_insert(Vec::new()).push(dir.to_string()); 89 | acc 90 | }); 91 | print_languages( 92 | lang_exts 93 | .into_iter() 94 | .map(|(lang, exts)| (lang, exts, lang_dirs.get(&lang).unwrap())) 95 | .collect(), 96 | ); 97 | } 98 | } 99 | 100 | impl fmt::Display for Language { 101 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 102 | write!( 103 | f, 104 | "{}", 105 | match self { 106 | Language::Bash => "Bash", 107 | Language::BibTeX => "BibTeX", 108 | Language::C => "C", 109 | Language::CSharp => "C#", 110 | Language::Clojure => "Clojure", 111 | Language::Cpp => "C++", 112 | Language::Css => "CSS", 113 | Language::Dart => "Dart", 114 | Language::Elm => "Elm", 115 | Language::Erlang => "Erlang", 116 | Language::EmbeddedTemplate => "Embedded Template", 117 | Language::Fennel => "Fennel", 118 | Language::GDScript => "GDScript", 119 | Language::Go => "Go", 120 | Language::GraphQL => "GraphQL", 121 | Language::Haskell => "Haskell", 122 | Language::Html => "HTML", 123 | Language::Java => "Java", 124 | Language::Javascript => "Javascript", 125 | Language::Json => "JSON", 126 | Language::Julia => "Julia", 127 | Language::Kotlin => "Kotlin", 128 | Language::LaTeX => "LaTeX", 129 | Language::Lua => "Lua", 130 | Language::Markdown => "Markdown", 131 | Language::Nix => "Nix", 132 | Language::OCaml => "OCaml", 133 | Language::OCamlInterface => "OCaml Interface", 134 | Language::OCamlLex => "OCamlLex", 135 | Language::Php => "PHP", 136 | Language::Python => "Python", 137 | Language::Query => "Tree-sitter Query", 138 | Language::R => "R", 139 | Language::Rst => "RST", 140 | Language::Ruby => "Ruby", 141 | Language::Rust => "Rust", 142 | Language::Scala => "Scala", 143 | Language::Scss => "SCSS", 144 | Language::Supercollider => "Supercollider", 145 | Language::Svelte => "Svelte", 146 | Language::Swift => "Swift", 147 | Language::Teal => "Teal", 148 | Language::Toml => "TOML", 149 | Language::Typescript => "Typescript", 150 | Language::Tsx => "TSX", 151 | Language::Verilog => "Verilog", 152 | Language::Vue => "Vue", 153 | Language::Yaml => "YAML", 154 | Language::Zig => "Zig", 155 | Language::Unsupported => "Unsupported", 156 | } 157 | ) 158 | } 159 | } 160 | 161 | impl Language { 162 | pub fn get_treesitter_language(&self) -> Result { 163 | match self { 164 | Language::Bash => Ok(tree_sitter_bash::language()), 165 | Language::BibTeX => Ok(tree_sitter_bibtex::language()), 166 | Language::C => Ok(tree_sitter_c::language()), 167 | Language::CSharp => Ok(tree_sitter_c_sharp::language()), 168 | Language::Clojure => Ok(tree_sitter_clojure::language()), 169 | Language::Cpp => Ok(tree_sitter_cpp::language()), 170 | Language::Css => Ok(tree_sitter_css::language()), 171 | Language::Elm => Ok(tree_sitter_elm::language()), 172 | // Language::EmbeddedTemplate => Ok(tree_sitter_embedded_template::language()), 173 | Language::Erlang => Ok(tree_sitter_erlang::language()), 174 | Language::Go => Ok(tree_sitter_go::language()), 175 | Language::Html => Ok(tree_sitter_html::language()), 176 | Language::Java => Ok(tree_sitter_java::language()), 177 | Language::Javascript => Ok(tree_sitter_javascript::language()), 178 | Language::Json => Ok(tree_sitter_json::language()), 179 | Language::Julia => Ok(tree_sitter_julia::language()), 180 | Language::LaTeX => Ok(tree_sitter_latex::language()), 181 | Language::Markdown => Ok(tree_sitter_markdown::language()), 182 | Language::OCaml => Ok(tree_sitter_ocaml::language_ocaml()), 183 | Language::OCamlInterface => Ok(tree_sitter_ocaml::language_ocaml_interface()), 184 | Language::Python => Ok(tree_sitter_python::language()), 185 | Language::Query => Ok(tree_sitter_query::language()), 186 | Language::Ruby => Ok(tree_sitter_ruby::language()), 187 | Language::Rust => Ok(tree_sitter_rust::language()), 188 | Language::Scala => Ok(tree_sitter_scala::language()), 189 | Language::Svelte => Ok(tree_sitter_svelte::language()), 190 | Language::Typescript => Ok(tree_sitter_typescript::language_typescript()), 191 | // Language::TSX => Ok(tree_sitter_typescript::language_tsx()), 192 | _ => Err(Error::UnsupportedLanguage), 193 | } 194 | } 195 | } 196 | 197 | impl From<&Path> for Language { 198 | fn from(path: &Path) -> Language { 199 | let (tag, map) = if path.is_dir() { 200 | // we assign a `Language` to query directories which take the form {query dir}/{language}/{query name}.scm 201 | // the {query name}.scm has already been stripped since this is a directory 202 | (path.file_name(), &DIR_TO_LANGUAGE) 203 | } else { 204 | // we only check the extension to determine language 205 | // TODO this could be improved on by looking for she-bangs, Vim modelines, or Emacs modelines, 206 | // among a few other more complicated heuristics 207 | (path.extension(), &EXT_TO_LANGUAGE) 208 | }; 209 | let tag = tag.map(OsString::from).unwrap_or(OsString::new()); 210 | map.get(tag.to_string_lossy().as_ref()) 211 | .unwrap_or(&Language::Unsupported) 212 | .clone() 213 | } 214 | } 215 | 216 | static EXT_TO_LANGUAGE: phf::Map<&'static str, Language> = phf::phf_map! { 217 | "bash" => Language::Bash, 218 | "bib" => Language::BibTeX, 219 | "c" => Language::C, 220 | "h" => Language::C, 221 | "cs" => Language::CSharp, 222 | "csx" => Language::CSharp, 223 | "clj" => Language::Clojure, 224 | "cc" => Language::Cpp, 225 | "cpp" => Language::Cpp, 226 | "cxx" => Language::Cpp, 227 | "c++" => Language::Cpp, 228 | "hh" => Language::Cpp, 229 | "hpp" => Language::Cpp, 230 | "h++" => Language::Cpp, 231 | "css" => Language::Css, 232 | // "dart" => Language::Dart, 233 | "elm" => Language::Elm, 234 | "erl" => Language::Erlang, 235 | "hrl" => Language::Erlang, 236 | // "erb" => Language::EmbeddedTemplate, 237 | // "ejs" => Language::EmbeddedTemplate, 238 | // "fnl" => Language::Fennel, 239 | // "gd" => Language::GDScript, 240 | "go" => Language::Go, 241 | // "graphql" => Language::GraphQL, 242 | // "hs" => Language::Haskell, 243 | "html" => Language::Html, 244 | "java" => Language::Java, 245 | "js" => Language::Javascript, 246 | "mjs" => Language::Javascript, 247 | "json" => Language::Json, 248 | "jl" => Language::Julia, 249 | // "kt" => Language::Kotlin, 250 | // "kts" => Language::Kotlin, 251 | "tex" => Language::LaTeX, 252 | // "lua" => Language::Lua, 253 | "md" => Language::Markdown, 254 | // "nix" => Language::Nix, 255 | "ml" => Language::OCaml, 256 | "mli" => Language::OCamlInterface, 257 | // "mll" => Language::OCamlLex, 258 | // "php" => Language::PHP, 259 | "py" => Language::Python, 260 | "pyw" => Language::Python, 261 | "scm" => Language::Query, 262 | // "r" => Language::R, 263 | // "rst" => Language::RST, 264 | "rb" => Language::Ruby, 265 | "rs" => Language::Rust, 266 | "sc" => Language::Scala, 267 | "scala" => Language::Scala, 268 | // "scss" => Language::SCSS, 269 | "svelte" => Language::Svelte, 270 | // "swift" => Language::Swift, 271 | // "tl" => Language::Teal, 272 | // "toml" => Language::TOML, 273 | "ts" => Language::Typescript, 274 | // "tsx" => Language::TSX, 275 | // "v" => Language::Verilog, 276 | // "vg" => Language::Verilog, 277 | // "vh" => Language::Verilog, 278 | // "vue" => Language::Vue, 279 | // "yaml" => Language::YAML, 280 | // "zig" => Language::Zig, 281 | }; 282 | 283 | /// These dir names are used when reading query files to know what language they refer to. 284 | /// See tcount --help for more information on queries. 285 | static DIR_TO_LANGUAGE: phf::Map<&'static str, Language> = phf::phf_map! { 286 | "bash" => Language::Bash, 287 | "bibtex" => Language::BibTeX, 288 | "c" => Language::C, 289 | "c_sharp" => Language::CSharp, 290 | "clojure" => Language::Clojure, 291 | "cpp" => Language::Cpp, 292 | "css" => Language::Css, 293 | "dart" => Language::Dart, 294 | "elm" => Language::Elm, 295 | "erlang" => Language::Erlang, 296 | "embeddedtemplate" => Language::EmbeddedTemplate, 297 | "fennel" => Language::Fennel, 298 | "gdscript" => Language::GDScript, 299 | "go" => Language::Go, 300 | "graphql" => Language::GraphQL, 301 | "haskell" => Language::Haskell, 302 | "html" => Language::Html, 303 | "java" => Language::Java, 304 | "javascript" => Language::Javascript, 305 | "json" => Language::Json, 306 | "julia" => Language::Julia, 307 | "kotlin" => Language::Kotlin, 308 | "latex" => Language::LaTeX, 309 | "lua" => Language::Lua, 310 | "markdown" => Language::Markdown, 311 | "nix" => Language::Nix, 312 | "ocaml" => Language::OCaml, 313 | "ocaml_interface" => Language::OCamlInterface, 314 | "ocamllex" => Language::OCamlLex, 315 | "php" => Language::Php, 316 | "python" => Language::Python, 317 | "query" => Language::Query, 318 | "r" => Language::R, 319 | "rst" => Language::Rst, 320 | "ruby" => Language::Ruby, 321 | "rust" => Language::Rust, 322 | "scala" => Language::Scala, 323 | "scss" => Language::Scss, 324 | "supercollider" => Language::Supercollider, 325 | "svelte" => Language::Svelte, 326 | "swift" => Language::Swift, 327 | "teal" => Language::Teal, 328 | "toml" => Language::Toml, 329 | "typescript" => Language::Typescript, 330 | "tsx" => Language::Tsx, 331 | "verilog" => Language::Verilog, 332 | "vue" => Language::Vue, 333 | "yaml" => Language::Yaml, 334 | "zig" => Language::Zig, 335 | }; 336 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use rayon::prelude::*; 2 | use std::collections::{HashMap, HashSet}; 3 | use std::iter::FromIterator; 4 | use std::path::{Path, PathBuf}; 5 | use std::process; 6 | use structopt::StructOpt; 7 | 8 | mod cli; 9 | mod count; 10 | mod error; 11 | mod fs; 12 | mod language; 13 | mod output; 14 | mod query; 15 | mod tree; 16 | 17 | use cli::{GroupBy, SortBy}; 18 | use count::Counts; 19 | use error::{Error, Result}; 20 | use language::Language; 21 | use output::print; 22 | 23 | fn get_counts_for_paths( 24 | paths: &[impl AsRef], 25 | cli: &cli::Cli, 26 | whitelist: &HashSet, 27 | blacklist: &HashSet, 28 | ) -> (Vec<(Language, PathBuf, Counts)>, Vec) { 29 | let (file_counts, errors): (Vec<_>, Vec<_>) = fs::iter_paths( 30 | paths, 31 | cli.no_git, 32 | cli.count_hidden, 33 | cli.no_dot_ignore, 34 | cli.no_parent_ignore, 35 | ) 36 | .map(|res| { 37 | let path = res?; 38 | let lang = Language::from(path.as_ref()); 39 | let ignore_path = if whitelist.is_empty() { 40 | blacklist.is_empty() || !blacklist.contains(&lang.to_string()) 41 | } else { 42 | whitelist.contains(&lang.to_string()) 43 | }; 44 | 45 | if ignore_path { 46 | let counts = Counts::from_path(&path, &lang, &cli.kind, &cli.kind_pattern, &cli.query)?; 47 | Ok((lang, path, counts)) 48 | } else { 49 | Err(Error::LanguageIgnored(path, lang)) 50 | } 51 | }) 52 | .partition(Result::is_ok); 53 | ( 54 | file_counts.into_iter().map(Result::unwrap).collect(), 55 | errors.into_iter().map(Result::unwrap_err).collect(), 56 | ) 57 | } 58 | 59 | fn run(cli: cli::Cli) -> Result<()> { 60 | let whitelist: HashSet = HashSet::from_iter(cli.whitelist.iter().cloned()); 61 | let blacklist: HashSet = HashSet::from_iter(cli.blacklist.iter().cloned()); 62 | 63 | let (mut counts, errors): (Vec<(String, Counts)>, Vec) = match cli.groupby { 64 | GroupBy::Language => { 65 | let (counts, errors) = get_counts_for_paths(&cli.paths, &cli, &whitelist, &blacklist); 66 | let counts = counts 67 | .into_iter() 68 | .fold(HashMap::new(), |mut acc, (lang, _path, counts)| { 69 | if let Some(cur) = acc.get_mut(&lang.to_string()) { 70 | *cur += counts; 71 | } else { 72 | acc.insert(lang.to_string(), counts); 73 | } 74 | acc 75 | }) 76 | .into_iter() 77 | .collect(); 78 | (counts, errors) 79 | } 80 | GroupBy::File => { 81 | let (counts, errors) = get_counts_for_paths(&cli.paths, &cli, &whitelist, &blacklist); 82 | let counts = counts 83 | .into_iter() 84 | .map(|(_lang, path, count)| (path.display().to_string(), count)) 85 | .collect(); 86 | (counts, errors) 87 | } 88 | GroupBy::Arg => { 89 | let (counts, errors): (Vec<_>, Vec<_>) = cli 90 | .paths 91 | .par_iter() 92 | .map(|path| { 93 | let (counts, errors) = 94 | get_counts_for_paths(&[path], &cli, &whitelist, &blacklist); 95 | let counts = counts.into_iter().fold( 96 | Counts::empty(cli.kind.len(), cli.kind_pattern.len(), &cli.query), 97 | |mut acc, (_lang, _path, counts)| { 98 | acc += counts; 99 | acc 100 | }, 101 | ); 102 | ((path.display().to_string(), counts), errors) 103 | }) 104 | .unzip(); 105 | (counts, errors.into_iter().flatten().collect()) 106 | } 107 | }; 108 | 109 | match cli.sort_by { 110 | // sort asc lexographical order on either language or file 111 | SortBy::Group => counts.sort_by(|(l1, _c1), (l2, _c2)| l1.cmp(l2)), 112 | // sort desc numerical order 113 | SortBy::NumFiles => counts.sort_by(|(_l1, c1), (_l2, c2)| c2.nfiles.cmp(&c1.nfiles)), 114 | // sort desc numerical order 115 | SortBy::Tokens => counts.sort_by(|(_l1, c1), (_l2, c2)| c2.ntokens.cmp(&c1.ntokens)), 116 | } 117 | 118 | let totals: Option = if cli.show_totals { 119 | Some(counts.iter().fold( 120 | Counts::empty(cli.kind.len(), cli.kind_pattern.len(), &cli.query), 121 | |mut cur, (_, counts)| { 122 | cur += counts.clone(); 123 | cur 124 | }, 125 | )) 126 | } else { 127 | None 128 | }; 129 | 130 | let counts = if let Some(n) = cli.top { 131 | counts.into_iter().take(n).collect() 132 | } else { 133 | counts 134 | }; 135 | 136 | if !counts.is_empty() { 137 | print( 138 | &cli.format, 139 | counts, 140 | totals, 141 | &cli.kind, 142 | &cli.kind_pattern, 143 | &cli.query, 144 | ); 145 | } else { 146 | println!("No files found."); 147 | } 148 | 149 | errors 150 | .into_iter() 151 | .filter(|err| err.should_show(cli.verbose)) 152 | .for_each(|err| { 153 | eprintln!("{}", err); 154 | }); 155 | Ok(()) 156 | } 157 | 158 | fn main() { 159 | let cli = cli::Cli::from_args(); 160 | 161 | if cli.list_languages { 162 | Language::print_all(); 163 | } else if let Err(err) = run(cli) { 164 | eprintln!("{}", err); 165 | process::exit(1); 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /src/output.rs: -------------------------------------------------------------------------------- 1 | use crate::count::Counts; 2 | use crate::language::Language; 3 | use crate::query::{Query, QueryKind}; 4 | use prettytable::{format, Cell, Row, Table}; 5 | use regex::Regex; 6 | use std::fmt::Display; 7 | use std::format; 8 | use std::str::FromStr; 9 | 10 | #[derive(Debug)] 11 | pub enum Format { 12 | Table, 13 | Csv, 14 | } 15 | 16 | impl FromStr for Format { 17 | type Err = String; 18 | 19 | fn from_str(s: &str) -> Result { 20 | match s { 21 | "table" => Ok(Format::Table), 22 | "csv" => Ok(Format::Csv), 23 | _ => Err(format!("\"{}\" is not supported. Use one of table|csv", s)), 24 | } 25 | } 26 | } 27 | 28 | pub fn format_builder() -> format::FormatBuilder { 29 | format::FormatBuilder::new() 30 | .separators( 31 | &[format::LinePosition::Top], 32 | format::LineSeparator::new('─', '─', '─', '─'), 33 | ) 34 | .separators( 35 | &[format::LinePosition::Title], 36 | format::LineSeparator::new('─', '─', '│', '│'), 37 | ) 38 | .separators( 39 | &[format::LinePosition::Bottom], 40 | format::LineSeparator::new('─', '─', '─', '─'), 41 | ) 42 | .padding(1, 1) 43 | } 44 | 45 | #[inline] 46 | fn title_cell(content: &str) -> Cell { 47 | Cell::new(content).style_spec("b") 48 | } 49 | #[inline] 50 | fn label_cell(label: &str) -> Cell { 51 | Cell::new(label).style_spec("li") 52 | } 53 | #[inline] 54 | fn count_cell(count: u64) -> Cell { 55 | Cell::new(&count.to_string()).style_spec("r") 56 | } 57 | 58 | #[inline] 59 | fn generic_cell(s: impl Display) -> Cell { 60 | Cell::new(&s.to_string()).style_spec("l") 61 | } 62 | 63 | pub fn print( 64 | format: &Format, 65 | counts: Vec<(String, Counts)>, 66 | totals: Option, 67 | kinds: &Vec, 68 | kind_patterns: &Vec, 69 | queries: &Vec, 70 | ) { 71 | let mut table = Table::new(); 72 | table.set_format(format_builder().build()); 73 | 74 | let mut titles = Vec::with_capacity(3 + kinds.len() + kind_patterns.len() + queries.len()); 75 | titles.push(title_cell("Group")); 76 | titles.push(title_cell("Files")); 77 | titles.push(title_cell("Tokens")); 78 | kinds 79 | .iter() 80 | .for_each(|kind| titles.push(title_cell(&format!("Kind({})", kind)))); 81 | kind_patterns 82 | .iter() 83 | .for_each(|kind_pat| titles.push(title_cell(&format!("Pattern({})", kind_pat)))); 84 | queries.iter().for_each(|query| match &query.kind { 85 | QueryKind::Match => titles.push(title_cell(&format!("Query({})", query.name))), 86 | QueryKind::Captures(names) => names.iter().for_each(|name| { 87 | titles.push(title_cell(&format!("Query({}@{})", query.name, name))); 88 | }), 89 | }); 90 | table.set_titles(Row::new(titles)); 91 | 92 | counts 93 | .iter() 94 | .chain( 95 | { 96 | if let Some(totals) = totals { 97 | vec![(String::from("TOTALS"), totals)] 98 | } else { 99 | vec![] 100 | } 101 | } 102 | .iter(), 103 | ) 104 | .map(|(label, count)| { 105 | let mut cols = 106 | Vec::with_capacity(3 + kinds.len() + kind_patterns.len() + queries.len()); 107 | 108 | // Language 109 | cols.push(label_cell(&label.to_string())); 110 | // number of files 111 | cols.push(count_cell(count.nfiles)); 112 | // number of tokens 113 | cols.push(count_cell(count.ntokens)); 114 | // number of nodes for a specific kind 115 | count.nkinds.iter().for_each(|n| cols.push(count_cell(*n))); 116 | // number of nodes for a specific pattern 117 | count 118 | .nkind_patterns 119 | .iter() 120 | .for_each(|n| cols.push(count_cell(*n))); 121 | // number of nodes for a specific query 122 | count 123 | .nqueries 124 | .iter() 125 | .for_each(|n| cols.push(count_cell(*n))); 126 | cols 127 | }) 128 | .for_each(|row| { 129 | table.add_row(Row::new(row)); 130 | }); 131 | 132 | match format { 133 | Format::Table => { 134 | table.printstd(); 135 | } 136 | Format::Csv => match table.to_csv(std::io::stdout()) { 137 | Ok(_) => {} 138 | Err(err) => eprintln!("{}", err), 139 | }, 140 | } 141 | } 142 | 143 | pub fn print_languages(langs: Vec<(&Language, Vec, &Vec)>) { 144 | let mut table = Table::new(); 145 | table.set_format(format_builder().build()); 146 | 147 | let titles = vec![ 148 | title_cell("Language"), 149 | title_cell("Extensions"), 150 | title_cell("Query Dir Name"), 151 | ]; 152 | table.set_titles(Row::new(titles)); 153 | 154 | langs.into_iter().for_each(|(lang, exts, dirs)| { 155 | let cols = vec![ 156 | label_cell(&lang.to_string()), 157 | generic_cell(exts.join(",")), 158 | generic_cell(dirs.join(",")), 159 | ]; 160 | table.add_row(Row::new(cols)); 161 | }); 162 | 163 | table.printstd(); 164 | } 165 | -------------------------------------------------------------------------------- /src/query.rs: -------------------------------------------------------------------------------- 1 | use crate::error::Result; 2 | use crate::language::Language; 3 | use std::collections::HashMap; 4 | use std::env::var; 5 | use std::fs; 6 | use std::path::PathBuf; 7 | use std::str::FromStr; 8 | 9 | #[derive(Debug, Eq, PartialEq)] 10 | pub enum QueryKind { 11 | Match, 12 | Captures(Vec), 13 | } 14 | 15 | #[derive(Debug)] 16 | pub struct Query { 17 | pub name: String, 18 | pub kind: QueryKind, 19 | pub langs: HashMap, 20 | } 21 | 22 | impl FromStr for Query { 23 | type Err = String; 24 | 25 | /// Searches for tree-sitter queries based on the based on the string argument with the syntax 26 | /// "{query name}(@{capture name}(,{capture_name})*)?" 27 | /// For example, 28 | /// "foo" -> Query name of "foo" 29 | /// "foo@bar" -> Query name of "foo" and a capture name of "bar" 30 | /// "foo@bar,baz" -> Query name of "foo" and capture names of "bar" and "baz" 31 | /// 32 | /// A query directory is as a directory with subdirectories that contain query files with the 33 | /// name "{query name}.scm". These subdirectories are named based on the language those queries 34 | /// are written for. For example, a query directory tcount_queries/ could have a "comments" query 35 | /// for rust and ruby named queries/rust/comments.scm and queries/ruby/comments.scm, respectively. 36 | /// 37 | /// When searching for these query files, first, the present working directory is searched for 38 | /// a query directory named .tcount_queries/, then $XDG_CONFIG_HOME/tcount (defaults to $HOME/.config/tcount) 39 | /// is searched for query directories that match $XDG_CONFIG_HOME/tcount/* (conflicting query files 40 | /// result in undefined behaviour). 41 | fn from_str(name: &str) -> std::result::Result { 42 | let (kind, name) = match name.find('@') { 43 | Some(i) => ( 44 | QueryKind::Captures(name[i + 1..].split(',').map(String::from).collect()), 45 | &name[..i], 46 | ), 47 | None => (QueryKind::Match, name), 48 | }; 49 | 50 | let queries: Option> = vec![ 51 | // look in pwd for a .tcount_queries/ dir 52 | format!(".tcount_queries/*/{}.scm", name), 53 | // look in $XDG_CONFIG_HOME/tcount/* for a dir with queries 54 | format!( 55 | "{}/tcount/*/*/{}.scm", 56 | if !var("XDG_CONFIG_HOME").unwrap_or(String::new()).is_empty() { 57 | var("XDG_CONFIG_HOME").unwrap() 58 | } else { 59 | "~/.config".into() 60 | }, 61 | name 62 | ), 63 | ] 64 | .iter() 65 | .map(|dir_glob| glob::glob(dir_glob.as_str())) 66 | .filter_map(|res| res.ok()) 67 | .map(|entries| { 68 | entries 69 | .into_iter() 70 | .filter_map(|res| res.ok()) 71 | .map(|path| { 72 | let lang = Language::from(path.parent().unwrap_or(&PathBuf::new())); 73 | let tree_sitter_lang = lang.get_treesitter_language()?; 74 | let query_str = fs::read_to_string(&path)?; 75 | let mut query = tree_sitter::Query::new(tree_sitter_lang, &query_str)?; 76 | match &kind { 77 | QueryKind::Captures(captures) => { 78 | // Disable all captures that aren't used. 79 | let unused_captures: Vec = query 80 | .capture_names() 81 | .iter() 82 | .filter(|name| !captures.contains(name)) 83 | .map(String::clone) 84 | .collect(); 85 | unused_captures 86 | .iter() 87 | .for_each(|name| query.disable_capture(name)); 88 | } 89 | QueryKind::Match => { 90 | let names: Vec = query.capture_names().into(); 91 | names.iter().for_each(|name| query.disable_capture(name)); 92 | } 93 | } 94 | Ok((lang, query)) 95 | }) 96 | .filter_map(Result::ok) 97 | .collect() 98 | }) 99 | .find(|map: &HashMap| !map.is_empty()); 100 | 101 | if let Some(queries) = queries { 102 | Ok(Query { 103 | name: name.to_string(), 104 | kind, 105 | langs: queries, 106 | }) 107 | } else { 108 | Err(format!("Unabled to find query for {}", name)) 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/tree.rs: -------------------------------------------------------------------------------- 1 | use tree_sitter::{Node, Tree, TreeCursor}; 2 | 3 | pub struct TreeIterator<'a> { 4 | cursor: TreeCursor<'a>, 5 | next: Option>, 6 | } 7 | 8 | impl<'a> TreeIterator<'a> { 9 | pub fn new(tree: &'a Tree) -> Self { 10 | let cursor = tree.walk(); 11 | Self { 12 | next: Some(cursor.node()), 13 | cursor, 14 | } 15 | } 16 | } 17 | 18 | impl<'a> Iterator for TreeIterator<'a> { 19 | type Item = Node<'a>; 20 | 21 | fn next(&mut self) -> Option { 22 | if let Some(next) = self.next { 23 | let cursor = &mut self.cursor; 24 | // preoder traverse to find next node 25 | self.next = if !cursor.goto_first_child() && !cursor.goto_next_sibling() { 26 | // look for a parent with a sibling that we have yet to visit 27 | loop { 28 | if !cursor.goto_parent() { 29 | // we are back at the root of the syntax tree and preorder traversal is 30 | // done 31 | break None; 32 | } 33 | if cursor.goto_next_sibling() { 34 | break Some(cursor.node()); 35 | } 36 | } 37 | } else { 38 | Some(cursor.node()) 39 | }; 40 | Some(next) 41 | } else { 42 | None 43 | } 44 | } 45 | } 46 | 47 | #[cfg(test)] 48 | mod tests { 49 | use super::*; 50 | 51 | #[test] 52 | fn test_preorder_traversal() { 53 | let text = r" 54 | fn main() { 55 | let foo = 1; 56 | let bar = if foo > 2 { 57 | true 58 | } else { 59 | false 60 | }; 61 | }"; 62 | let mut parser = tree_sitter::Parser::new(); 63 | parser.set_language(tree_sitter_rust::language()).unwrap(); 64 | let tree = parser.parse(text, None).unwrap(); 65 | let tree_iter = TreeIterator::new(&tree); 66 | let kinds = vec![ 67 | "source_file", 68 | "function_item", 69 | "fn", 70 | "identifier", 71 | "parameters", 72 | "(", 73 | ")", 74 | "block", 75 | "{", 76 | "let_declaration", 77 | "let", 78 | "identifier", 79 | "=", 80 | "integer_literal", 81 | ";", 82 | "let_declaration", 83 | "let", 84 | "identifier", 85 | "=", 86 | "if_expression", 87 | "if", 88 | "binary_expression", 89 | "identifier", 90 | ">", 91 | "integer_literal", 92 | "block", 93 | "{", 94 | "boolean_literal", 95 | "true", 96 | "}", 97 | "else_clause", 98 | "else", 99 | "block", 100 | "{", 101 | "boolean_literal", 102 | "false", 103 | "}", 104 | ";", 105 | "}", 106 | ]; 107 | assert_eq!( 108 | kinds, 109 | tree_iter.map(|node| node.kind()).collect::>() 110 | ); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /tests/fixtures/.ignore: -------------------------------------------------------------------------------- 1 | .tc_queries 2 | /xdg_config_home 3 | /cargo_manifest_dir 4 | /foo 5 | -------------------------------------------------------------------------------- /tests/fixtures/.tcount_queries/README.md: -------------------------------------------------------------------------------- 1 | These are mainly used for tests 2 | -------------------------------------------------------------------------------- /tests/fixtures/.tcount_queries/go/_test.scm: -------------------------------------------------------------------------------- 1 | (comment) @pwd.test 2 | (comment) @pwd.test2 3 | -------------------------------------------------------------------------------- /tests/fixtures/.tcount_queries/ruby/_test.scm: -------------------------------------------------------------------------------- 1 | (comment) @pwd.test 2 | (comment) @pwd.test2 3 | -------------------------------------------------------------------------------- /tests/fixtures/.tcount_queries/rust/_test.scm: -------------------------------------------------------------------------------- 1 | [ (line_comment) (block_comment) ] @pwd.test 2 | [ (line_comment) (block_comment) ] @pwd.test2 3 | -------------------------------------------------------------------------------- /tests/fixtures/empty.rs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RRethy/tcount/341d9aa29512257bf7dfd7e843d02fdcfd583387/tests/fixtures/empty.rs -------------------------------------------------------------------------------- /tests/fixtures/foo/empty.rs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RRethy/tcount/341d9aa29512257bf7dfd7e843d02fdcfd583387/tests/fixtures/foo/empty.rs -------------------------------------------------------------------------------- /tests/fixtures/foo/invalid.rs: -------------------------------------------------------------------------------- 1 | use std::collections::BTreeMap; 2 | 3 | // This is a comment 4 | fn main() { 5 | // This is a multiline comment 6 | // This is a multiline comment 7 | let map = BTreeMap::new() 8 | /* 9 | this is a big comment 10 | */ 11 | map.insert("one\n", ); 12 | 13 | -------------------------------------------------------------------------------- /tests/fixtures/foo/ruby.rb: -------------------------------------------------------------------------------- 1 | class Foo 2 | def bar 3 | puts 'Hello, World!' 4 | # Hello, World! 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /tests/fixtures/foo/ruby1.rb: -------------------------------------------------------------------------------- 1 | require 'foobar' 2 | 3 | def foo() 4 | puts 'hello' 5 | if true then 6 | puts 'aaa' 7 | elsif true then 8 | puts 'bbb' 9 | else 10 | puts 'ccc' 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /tests/fixtures/foo/rust1.rs: -------------------------------------------------------------------------------- 1 | use std::collections::BTreeMap; 2 | 3 | // This is a comment 4 | fn main() { 5 | // This is a multiline comment 6 | // This is a multiline comment 7 | let map = BTreeMap::new(); 8 | /* 9 | this is a big comment 10 | */ 11 | map.insert("one\n", r"two\n"); 12 | } 13 | -------------------------------------------------------------------------------- /tests/fixtures/foo/rust2.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | pub struct Cli { 4 | pub files: Vec, 5 | } 6 | -------------------------------------------------------------------------------- /tests/fixtures/foo/rust3.rs: -------------------------------------------------------------------------------- 1 | use std::collections::BTreeMap; 2 | 3 | // This is a comment 4 | fn main() { 5 | let x = 3; 6 | if x < 3 { 7 | } else if x == 4 { 8 | } else { 9 | } 10 | loop {} 11 | 12 | // This is a multiline comment 13 | // This is a multiline comment 14 | let map = BTreeMap::new(); 15 | for x in map.iter() {} 16 | while true { 17 | break; 18 | } 19 | /* 20 | this is a big comment 21 | */ 22 | map.insert("one\n", r"two\n"); 23 | } 24 | -------------------------------------------------------------------------------- /tests/fixtures/go1.go: -------------------------------------------------------------------------------- 1 | fn main() { 2 | if true { 3 | fmt.Println("") 4 | } else if true { 5 | fmt.Println("") 6 | } else { 7 | fmt.Println("") 8 | } 9 | fmt.Println("") 10 | } 11 | -------------------------------------------------------------------------------- /tests/fixtures/invalid.rs: -------------------------------------------------------------------------------- 1 | use std::collections::BTreeMap; 2 | 3 | // This is a comment 4 | fn main() { 5 | // This is a multiline comment 6 | // This is a multiline comment 7 | let map = BTreeMap::new() 8 | /* 9 | this is a big comment 10 | */ 11 | map.insert("one\n", ); 12 | 13 | -------------------------------------------------------------------------------- /tests/fixtures/ruby.rb: -------------------------------------------------------------------------------- 1 | class Foo 2 | def bar 3 | puts 'Hello, World!' 4 | # Hello, World! 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /tests/fixtures/ruby1.rb: -------------------------------------------------------------------------------- 1 | require 'foobar' 2 | 3 | def foo() 4 | puts 'hello' 5 | if true then 6 | puts 'aaa' 7 | elsif true then 8 | puts 'bbb' 9 | else 10 | puts 'ccc' 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /tests/fixtures/rust1.rs: -------------------------------------------------------------------------------- 1 | use std::collections::BTreeMap; 2 | 3 | // This is a comment 4 | fn main() { 5 | // This is a multiline comment 6 | // This is a multiline comment 7 | let map = BTreeMap::new(); 8 | /* 9 | this is a big comment 10 | */ 11 | map.insert("one\n", r"two\n"); 12 | } 13 | -------------------------------------------------------------------------------- /tests/fixtures/rust2.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | pub struct Cli { 4 | pub files: Vec, 5 | } 6 | -------------------------------------------------------------------------------- /tests/fixtures/rust3.rs: -------------------------------------------------------------------------------- 1 | use std::collections::BTreeMap; 2 | 3 | // This is a comment 4 | fn main() { 5 | let x = 3; 6 | if x < 3 { 7 | } else if x == 4 { 8 | } else { 9 | } 10 | loop {} 11 | 12 | // This is a multiline comment 13 | // This is a multiline comment 14 | let map = BTreeMap::new(); 15 | for x in map.iter() {} 16 | while true { 17 | break; 18 | } 19 | /* 20 | this is a big comment 21 | */ 22 | map.insert("one\n", r"two\n"); 23 | } 24 | -------------------------------------------------------------------------------- /tests/fixtures/unsupported.abc: -------------------------------------------------------------------------------- 1 | pub fun inc(n) = n + 1 2 | pub fun dec(n) = n - 1 3 | -------------------------------------------------------------------------------- /tests/fixtures/xdg_config_home/tcount/queries/go/_test.scm: -------------------------------------------------------------------------------- 1 | (comment) @xdg.test 2 | -------------------------------------------------------------------------------- /tests/fixtures/xdg_config_home/tcount/queries/go/string.scm: -------------------------------------------------------------------------------- 1 | (raw_string_literal) @xdg.string 2 | -------------------------------------------------------------------------------- /tests/fixtures/xdg_config_home/tcount/queries/ruby/_test.scm: -------------------------------------------------------------------------------- 1 | (comment) @xdg.test 2 | -------------------------------------------------------------------------------- /tests/fixtures/xdg_config_home/tcount/queries/ruby/string.scm: -------------------------------------------------------------------------------- 1 | (string) @xdg.string 2 | -------------------------------------------------------------------------------- /tests/fixtures/xdg_config_home/tcount/queries/rust/_test.scm: -------------------------------------------------------------------------------- 1 | [ (line_comment) (block_comment) ] @xdg.test 2 | -------------------------------------------------------------------------------- /tests/fixtures/xdg_config_home/tcount/queries/rust/string.scm: -------------------------------------------------------------------------------- 1 | (string_literal) @xdg.string 2 | -------------------------------------------------------------------------------- /tests/main.rs: -------------------------------------------------------------------------------- 1 | mod utils; 2 | 3 | use utils::tcount; 4 | 5 | #[test] 6 | fn test_whitelist() { 7 | tcount() 8 | .current_dir("tests/fixtures/") 9 | .args(["--format", "csv", "--whitelist", "Rust", "Ruby"].iter()) 10 | .assert() 11 | .stdout( 12 | r"Group,Files,Tokens 13 | Rust,5,156 14 | Ruby,2,43 15 | ", 16 | ) 17 | .success(); 18 | } 19 | 20 | #[test] 21 | fn test_blacklist() { 22 | tcount() 23 | .current_dir("tests/fixtures/") 24 | .args(["--format", "csv", "--blacklist", "Rust", "Ruby"].iter()) 25 | .assert() 26 | .stdout( 27 | r"Group,Files,Tokens 28 | Go,1,52 29 | Unsupported,1,0 30 | ", 31 | ) 32 | .success(); 33 | } 34 | 35 | #[test] 36 | fn test_groupby_language() { 37 | tcount() 38 | .current_dir("tests/fixtures/") 39 | .args(["--format", "csv", "--groupby", "language"].iter()) 40 | .assert() 41 | .stdout( 42 | r"Group,Files,Tokens 43 | Rust,5,156 44 | Go,1,52 45 | Ruby,2,43 46 | Unsupported,1,0 47 | ", 48 | ) 49 | .success(); 50 | } 51 | 52 | #[test] 53 | fn test_groupby_file() { 54 | tcount() 55 | .current_dir("tests/fixtures/") 56 | .args(["--format", "csv", "--groupby", "file", "--whitelist", "Go"].iter()) 57 | .assert() 58 | .stdout( 59 | r"Group,Files,Tokens 60 | ./go1.go,1,52 61 | ", 62 | ) 63 | .success(); 64 | } 65 | 66 | #[test] 67 | fn test_groupby_arguments() { 68 | tcount() 69 | .current_dir("tests/fixtures/") 70 | .args( 71 | [ 72 | "--format", 73 | "csv", 74 | "--groupby", 75 | "arg", 76 | "--", 77 | "go1.go", 78 | "foo", 79 | "ruby.rb", 80 | ] 81 | .iter(), 82 | ) 83 | .assert() 84 | .stdout( 85 | r"Group,Files,Tokens 86 | foo,7,199 87 | go1.go,1,52 88 | ruby.rb,1,10 89 | ", 90 | ) 91 | .success(); 92 | } 93 | 94 | #[test] 95 | fn test_sortby_group() { 96 | tcount() 97 | .current_dir("tests/fixtures/") 98 | .args(["--format", "csv", "--sort-by", "group"].iter()) 99 | .assert() 100 | .stdout( 101 | r"Group,Files,Tokens 102 | Go,1,52 103 | Ruby,2,43 104 | Rust,5,156 105 | Unsupported,1,0 106 | ", 107 | ) 108 | .success(); 109 | } 110 | 111 | #[test] 112 | fn test_sortby_numfiles() { 113 | tcount() 114 | .current_dir("tests/fixtures/") 115 | .args( 116 | [ 117 | "--format", 118 | "csv", 119 | "--sort-by", 120 | "numfiles", 121 | "--whitelist", 122 | "Rust", 123 | "Ruby", 124 | "Go", 125 | ] 126 | .iter(), 127 | ) 128 | .assert() 129 | .stdout( 130 | r"Group,Files,Tokens 131 | Rust,5,156 132 | Ruby,2,43 133 | Go,1,52 134 | ", 135 | ) 136 | .success(); 137 | } 138 | 139 | #[test] 140 | fn test_sortby_tokens() { 141 | tcount() 142 | .current_dir("tests/fixtures/") 143 | .args(["--format", "csv", "--sort-by", "tokens"].iter()) 144 | .assert() 145 | .stdout( 146 | r"Group,Files,Tokens 147 | Rust,5,156 148 | Go,1,52 149 | Ruby,2,43 150 | Unsupported,1,0 151 | ", 152 | ) 153 | .success(); 154 | } 155 | 156 | #[test] 157 | fn test_show_totals() { 158 | tcount() 159 | .current_dir("tests/fixtures/") 160 | .args(["--format", "csv", "--show-totals"].iter()) 161 | .assert() 162 | .stdout( 163 | r"Group,Files,Tokens 164 | Rust,5,156 165 | Go,1,52 166 | Ruby,2,43 167 | Unsupported,1,0 168 | TOTALS,9,251 169 | ", 170 | ) 171 | .success(); 172 | } 173 | 174 | #[test] 175 | fn test_count_node_kinds() { 176 | tcount() 177 | .current_dir("tests/fixtures/") 178 | .args(["--format", "csv", "--kind", "line_comment"].iter()) 179 | .assert() 180 | .stdout( 181 | r"Group,Files,Tokens,Kind(line_comment) 182 | Rust,5,156,9 183 | Go,1,52,0 184 | Ruby,2,43,0 185 | Unsupported,1,0,0 186 | ", 187 | ) 188 | .success(); 189 | } 190 | 191 | #[test] 192 | fn test_count_node_kind_patterns() { 193 | tcount() 194 | .current_dir("tests/fixtures/") 195 | .args(["--format", "csv", "--kind-pattern", ".*comment.*"].iter()) 196 | .assert() 197 | .stdout( 198 | r"Group,Files,Tokens,Pattern(.*comment.*) 199 | Rust,5,156,12 200 | Go,1,52,0 201 | Ruby,2,43,1 202 | Unsupported,1,0,0 203 | ", 204 | ) 205 | .success(); 206 | } 207 | 208 | #[test] 209 | fn test_top_n() { 210 | tcount() 211 | .current_dir("tests/fixtures/") 212 | .args(["--format", "csv", "--top", "2"].iter()) 213 | .assert() 214 | .stdout( 215 | r"Group,Files,Tokens 216 | Rust,5,156 217 | Go,1,52 218 | ", 219 | ) 220 | .success(); 221 | } 222 | -------------------------------------------------------------------------------- /tests/output.rs: -------------------------------------------------------------------------------- 1 | mod utils; 2 | 3 | use utils::tcount; 4 | 5 | #[test] 6 | fn test_format_csv() { 7 | let expected = r"Group,Files,Tokens 8 | Rust,5,156 9 | Go,1,52 10 | Ruby,2,43 11 | Unsupported,1,0 12 | "; 13 | 14 | tcount() 15 | .current_dir("tests/fixtures") 16 | .args(["--format", "csv"].iter()) 17 | .assert() 18 | .stdout(expected) 19 | .success(); 20 | } 21 | 22 | #[test] 23 | fn test_format_table() { 24 | let expected = r"──────────────────────────── 25 | Group Files Tokens 26 | ──────────────────────────── 27 | Rust 5 156 28 | Go 1 52 29 | Ruby 2 43 30 | Unsupported 1 0 31 | ──────────────────────────── 32 | "; 33 | tcount() 34 | .current_dir("tests/fixtures") 35 | .args(["--format", "table"].iter()) 36 | .assert() 37 | .stdout(expected) 38 | .success(); 39 | } 40 | -------------------------------------------------------------------------------- /tests/query.rs: -------------------------------------------------------------------------------- 1 | mod utils; 2 | 3 | use utils::tcount; 4 | 5 | #[test] 6 | fn test_local_queries() { 7 | tcount() 8 | .current_dir("tests/fixtures/") 9 | .env( 10 | "XDG_CONFIG_HOME", 11 | format!( 12 | "{}/tests/fixtures/xdg_config_home", 13 | env!("CARGO_MANIFEST_DIR") 14 | ), 15 | ) 16 | .args( 17 | [ 18 | "--format", 19 | "csv", 20 | "--whitelist", 21 | "Rust", 22 | "Go", 23 | "Ruby", 24 | "--query", 25 | "_test", 26 | ] 27 | .iter(), 28 | ) 29 | .assert() 30 | .stdout( 31 | r"Group,Files,Tokens,Query(_test) 32 | Rust,5,156,24 33 | Go,1,52,0 34 | Ruby,2,43,2 35 | ", 36 | ) 37 | .success(); 38 | } 39 | 40 | #[test] 41 | fn test_xdg_queries() { 42 | tcount() 43 | .current_dir("tests/fixtures/") 44 | .env( 45 | "XDG_CONFIG_HOME", 46 | format!( 47 | "{}/tests/fixtures/xdg_config_home", 48 | env!("CARGO_MANIFEST_DIR") 49 | ), 50 | ) 51 | .args( 52 | [ 53 | "--format", 54 | "csv", 55 | "--whitelist", 56 | "Rust", 57 | "Go", 58 | "Ruby", 59 | "--query", 60 | "string", 61 | ] 62 | .iter(), 63 | ) 64 | .assert() 65 | .stdout( 66 | r"Group,Files,Tokens,Query(string) 67 | Rust,5,156,3 68 | Go,1,52,0 69 | Ruby,2,43,6 70 | ", 71 | ) 72 | .success(); 73 | } 74 | 75 | #[test] 76 | fn test_queries_with_captures() { 77 | tcount() 78 | .current_dir("tests/fixtures/") 79 | .env( 80 | "XDG_CONFIG_HOME", 81 | format!( 82 | "{}/tests/fixtures/xdg_config_home", 83 | env!("CARGO_MANIFEST_DIR") 84 | ), 85 | ) 86 | .args( 87 | [ 88 | "--format", 89 | "csv", 90 | "--whitelist", 91 | "Rust", 92 | "Go", 93 | "Ruby", 94 | "--query", 95 | "_test@pwd.test,pwd.test2", 96 | ] 97 | .iter(), 98 | ) 99 | .assert() 100 | .stdout( 101 | r"Group,Files,Tokens,Query(_test@pwd.test),Query(_test@pwd.test2) 102 | Rust,5,156,12,12 103 | Go,1,52,0,0 104 | Ruby,2,43,1,1 105 | ", 106 | ) 107 | .success(); 108 | } 109 | -------------------------------------------------------------------------------- /tests/utils/mod.rs: -------------------------------------------------------------------------------- 1 | pub fn tcount() -> assert_cmd::Command { 2 | assert_cmd::Command::cargo_bin(env!("CARGO_PKG_NAME")).unwrap() 3 | } 4 | --------------------------------------------------------------------------------