diff --git a/.github/workflows/codegraph-impact.yml b/.github/workflows/codegraph-impact.yml index a9edebd3..e063f8b6 100644 --- a/.github/workflows/codegraph-impact.yml +++ b/.github/workflows/codegraph-impact.yml @@ -28,7 +28,8 @@ jobs: path: .codegraph/ key: codegraph-${{ hashFiles('src/**', 'package.json') }} restore-keys: codegraph- - - run: npx codegraph build + - name: Build codegraph + run: npx codegraph build || (rm -rf .codegraph && npx codegraph build --no-incremental) - name: Run impact analysis run: | npx codegraph diff-impact --ref origin/${{ github.base_ref }} --json -T > impact.json || echo '{"affectedFiles":[],"summary":null}' > impact.json diff --git a/.npmrc b/.npmrc new file mode 100644 index 00000000..521a9f7c --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +legacy-peer-deps=true diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 00000000..89018d16 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,800 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "bitflags" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" + +[[package]] +name = "cc" +version = "1.2.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1e928d4b69e3077709075a938a05ffbedfa53a84c8f766efbf8220bb1ff60e1" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "codegraph-core" +version = "3.5.0" +dependencies = [ + "napi", + "napi-build", + "napi-derive", + "rayon", + "rusqlite", + "send_wrapper", + "serde", + "serde_json", + "tree-sitter", + "tree-sitter-bash", + "tree-sitter-c", + "tree-sitter-c-sharp", + "tree-sitter-cpp", + "tree-sitter-go", + "tree-sitter-hcl", + "tree-sitter-java", + "tree-sitter-javascript", + "tree-sitter-kotlin-sg", + "tree-sitter-php", + "tree-sitter-python", + "tree-sitter-ruby", + "tree-sitter-rust", + "tree-sitter-scala", + "tree-sitter-swift", + "tree-sitter-typescript", +] + +[[package]] +name = "convert_case" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "affbf0190ed2caf063e3def54ff444b449371d55c58e513a95ab98eca50adb49" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "ctor" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "352d39c2f7bef1d6ad73db6f5160efcaed66d94ef8c6c573a8410c00bf909a98" +dependencies = [ + "ctor-proc-macro", + "dtor", +] + +[[package]] +name = "ctor-proc-macro" +version = "0.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52560adf09603e58c9a7ee1fe1dcb95a16927b17c127f0ac02d6e768a0e25bc1" + +[[package]] +name = "dtor" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1057d6c64987086ff8ed0fd3fbf377a6b7d205cc7715868cd401705f715cbe4" +dependencies = [ + "dtor-proc-macro", +] + +[[package]] +name = "dtor-proc-macro" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f678cf4a922c215c63e0de95eb1ff08a958a81d47e485cf9da1e27bf6305cfa5" + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "fallible-iterator" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" + +[[package]] +name = "fallible-streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "futures" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-executor" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-macro" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash", +] + +[[package]] +name = "hashlink" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af" +dependencies = [ + "hashbrown", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "libloading" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "754ca22de805bb5744484a5b151a9e1a8e837d5dc232c2d7d8c2e3492edc8b60" +dependencies = [ + "cfg-if", + "windows-link", +] + +[[package]] +name = "libsqlite3-sys" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "napi" +version = "3.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb7848c221fb7bb789e02f01875287ebb1e078b92a6566a34de01ef8806e7c2b" +dependencies = [ + "bitflags", + "ctor", + "futures", + "napi-build", + "napi-sys", + "nohash-hasher", + "rustc-hash", + "serde", + "serde_json", +] + +[[package]] +name = "napi-build" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d376940fd5b723c6893cd1ee3f33abbfd86acb1cd1ec079f3ab04a2a3bc4d3b1" + +[[package]] +name = "napi-derive" +version = "3.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60867ff9a6f76e82350e0c3420cb0736f5866091b61d7d8a024baa54b0ec17dd" +dependencies = [ + "convert_case", + "ctor", + "napi-derive-backend", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "napi-derive-backend" +version = "5.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0864cf6a82e2cfb69067374b64c9253d7e910e5b34db833ed7495dda56ccb18" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "semver", + "syn", +] + +[[package]] +name = "napi-sys" +version = "3.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eb602b84d7c1edae45e50bbf1374696548f36ae179dfa667f577e384bb90c2b" +dependencies = [ + "libloading", +] + +[[package]] +name = "nohash-hasher" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451" + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rayon" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + +[[package]] +name = "rusqlite" +version = "0.32.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7753b721174eb8ff87a9a0e799e2d7bc3749323e773db92e0984debb00019d6e" +dependencies = [ + "bitflags", + "fallible-iterator", + "fallible-streaming-iterator", + "hashlink", + "libsqlite3-sys", + "smallvec", +] + +[[package]] +name = "rustc-hash" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" + +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" + +[[package]] +name = "send_wrapper" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd0b0ec5f1c1ca621c432a25813d8d60c88abe6d3e08a3eb9cf37d97a0fe3d73" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b2231b7c3057d5e4ad0156fb3dc807d900806020c5ffa3ee6ff2c8c76fb8520" + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tree-sitter" +version = "0.24.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5387dffa7ffc7d2dae12b50c6f7aab8ff79d6210147c6613561fc3d474c6f75" +dependencies = [ + "cc", + "regex", + "regex-syntax", + "streaming-iterator", + "tree-sitter-language", +] + +[[package]] +name = "tree-sitter-bash" +version = "0.23.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "329a4d48623ac337d42b1df84e81a1c9dbb2946907c102ca72db158c1964a52e" +dependencies = [ + "cc", + "tree-sitter-language", +] + +[[package]] +name = "tree-sitter-c" +version = "0.23.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afd2b1bf1585dc2ef6d69e87d01db8adb059006649dd5f96f31aa789ee6e9c71" +dependencies = [ + "cc", + "tree-sitter-language", +] + +[[package]] +name = "tree-sitter-c-sharp" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67f06accca7b45351758663b8215089e643d53bd9a660ce0349314263737fcb0" +dependencies = [ + "cc", + "tree-sitter-language", +] + +[[package]] +name = "tree-sitter-cpp" +version = "0.23.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df2196ea9d47b4ab4a31b9297eaa5a5d19a0b121dceb9f118f6790ad0ab94743" +dependencies = [ + "cc", + "tree-sitter-language", +] + +[[package]] +name = "tree-sitter-go" +version = "0.23.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b13d476345220dbe600147dd444165c5791bf85ef53e28acbedd46112ee18431" +dependencies = [ + "cc", + "tree-sitter-language", +] + +[[package]] +name = "tree-sitter-hcl" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a7b2cc3d7121553b84309fab9d11b3ff3d420403eef9ae50f9fd1cd9d9cf012" +dependencies = [ + "cc", + "tree-sitter-language", +] + +[[package]] +name = "tree-sitter-java" +version = "0.23.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0aa6cbcdc8c679b214e616fd3300da67da0e492e066df01bcf5a5921a71e90d6" +dependencies = [ + "cc", + "tree-sitter-language", +] + +[[package]] +name = "tree-sitter-javascript" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf40bf599e0416c16c125c3cec10ee5ddc7d1bb8b0c60fa5c4de249ad34dc1b1" +dependencies = [ + "cc", + "tree-sitter-language", +] + +[[package]] +name = "tree-sitter-kotlin-sg" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0e175b7530765d1e36ad234a7acaa8b2a3316153f239d724376c7ee5e8d8e98" +dependencies = [ + "cc", + "tree-sitter-language", +] + +[[package]] +name = "tree-sitter-language" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "009994f150cc0cd50ff54917d5bc8bffe8cad10ca10d81c34da2ec421ae61782" + +[[package]] +name = "tree-sitter-php" +version = "0.23.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f066e94e9272cfe4f1dcb07a1c50c66097eca648f2d7233d299c8ae9ed8c130c" +dependencies = [ + "cc", + "tree-sitter-language", +] + +[[package]] +name = "tree-sitter-python" +version = "0.23.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d065aaa27f3aaceaf60c1f0e0ac09e1cb9eb8ed28e7bcdaa52129cffc7f4b04" +dependencies = [ + "cc", + "tree-sitter-language", +] + +[[package]] +name = "tree-sitter-ruby" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be0484ea4ef6bb9c575b4fdabde7e31340a8d2dbc7d52b321ac83da703249f95" +dependencies = [ + "cc", + "tree-sitter-language", +] + +[[package]] +name = "tree-sitter-rust" +version = "0.23.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca8ccb3e3a3495c8a943f6c3fd24c3804c471fd7f4f16087623c7fa4c0068e8a" +dependencies = [ + "cc", + "tree-sitter-language", +] + +[[package]] +name = "tree-sitter-scala" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83079f50ea7d03e0faf6be6260ed97538e6df7349ec3cbcbf5771f7b38e3c8b7" +dependencies = [ + "cc", + "tree-sitter-language", +] + +[[package]] +name = "tree-sitter-swift" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ef216011c3e3df4fa864736f347cb8d509b1066cf0c8549fb1fd81ac9832e59" +dependencies = [ + "cc", + "tree-sitter-language", +] + +[[package]] +name = "tree-sitter-typescript" +version = "0.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c5f76ed8d947a75cc446d5fccd8b602ebf0cde64ccf2ffa434d873d7a575eff" +dependencies = [ + "cc", + "tree-sitter-language", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-segmentation" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "zerocopy" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/crates/codegraph-core/Cargo.toml b/crates/codegraph-core/Cargo.toml index 95ef3417..457f1e2a 100644 --- a/crates/codegraph-core/Cargo.toml +++ b/crates/codegraph-core/Cargo.toml @@ -22,6 +22,12 @@ tree-sitter-java = "0.23" tree-sitter-c-sharp = "0.23" tree-sitter-ruby = "0.23" tree-sitter-php = "0.23" +tree-sitter-c = "0.23" +tree-sitter-cpp = "0.23" +tree-sitter-kotlin-sg = "0.4" +tree-sitter-swift = "0.7" +tree-sitter-scala = "0.25" +tree-sitter-bash = "0.23" tree-sitter-hcl = "1" rayon = "1" # `bundled` embeds a second SQLite copy (better-sqlite3 already bundles one). diff --git a/crates/codegraph-core/src/cfg.rs b/crates/codegraph-core/src/cfg.rs index c60a8876..ab7cdb8b 100644 --- a/crates/codegraph-core/src/cfg.rs +++ b/crates/codegraph-core/src/cfg.rs @@ -330,6 +330,216 @@ pub static PHP_CFG: CfgRules = CfgRules { labeled_node: None, }; +pub static C_CFG: CfgRules = CfgRules { + if_node: Some("if_statement"), + if_nodes: &[], + elif_node: None, + else_clause: None, + else_via_alternative: true, + if_consequent_field: None, + for_nodes: &["for_statement"], + condition_field: Some("condition"), + while_node: Some("while_statement"), + while_nodes: &[], + do_node: Some("do_statement"), + infinite_loop_node: None, + unless_node: None, + until_node: None, + switch_node: Some("switch_statement"), + switch_nodes: &[], + case_node: Some("case_statement"), + case_nodes: &[], + default_node: Some("default_statement"), + wildcard_pattern_node: None, + try_node: None, + try_nodes: &[], + catch_node: None, + finally_node: None, + else_node: None, + return_node: Some("return_statement"), + throw_node: None, + break_node: Some("break_statement"), + continue_node: Some("continue_statement"), + block_node: Some("compound_statement"), + block_nodes: &[], + labeled_node: Some("labeled_statement"), +}; + +pub static CPP_CFG: CfgRules = CfgRules { + if_node: Some("if_statement"), + if_nodes: &[], + elif_node: None, + else_clause: None, + else_via_alternative: true, + if_consequent_field: None, + for_nodes: &["for_statement", "for_range_loop"], + condition_field: Some("condition"), + while_node: Some("while_statement"), + while_nodes: &[], + do_node: Some("do_statement"), + infinite_loop_node: None, + unless_node: None, + until_node: None, + switch_node: Some("switch_statement"), + switch_nodes: &[], + case_node: Some("case_statement"), + case_nodes: &[], + default_node: Some("default_statement"), + wildcard_pattern_node: None, + try_node: Some("try_statement"), + try_nodes: &[], + catch_node: Some("catch_clause"), + finally_node: None, + else_node: None, + return_node: Some("return_statement"), + throw_node: Some("throw_statement"), + break_node: Some("break_statement"), + continue_node: Some("continue_statement"), + block_node: Some("compound_statement"), + block_nodes: &[], + labeled_node: Some("labeled_statement"), +}; + +pub static KOTLIN_CFG: CfgRules = CfgRules { + if_node: Some("if_expression"), + if_nodes: &[], + elif_node: None, + else_clause: None, + else_via_alternative: true, + if_consequent_field: None, + for_nodes: &["for_statement"], + condition_field: Some("condition"), + while_node: Some("while_statement"), + while_nodes: &[], + do_node: Some("do_while_statement"), + infinite_loop_node: None, + unless_node: None, + until_node: None, + switch_node: Some("when_expression"), + switch_nodes: &[], + case_node: Some("when_entry"), + case_nodes: &[], + default_node: None, + wildcard_pattern_node: None, + try_node: Some("try_expression"), + try_nodes: &[], + catch_node: Some("catch_block"), + finally_node: Some("finally_block"), + else_node: None, + return_node: Some("jump_expression"), + throw_node: Some("throw_expression"), + break_node: Some("jump_expression"), + continue_node: Some("jump_expression"), + block_node: None, + block_nodes: &["statements"], + labeled_node: None, +}; + +pub static SWIFT_CFG: CfgRules = CfgRules { + if_node: Some("if_statement"), + if_nodes: &[], + elif_node: None, + else_clause: None, + else_via_alternative: true, + if_consequent_field: None, + for_nodes: &["for_in_statement"], + condition_field: Some("condition"), + while_node: Some("while_statement"), + while_nodes: &[], + do_node: Some("repeat_while_statement"), + infinite_loop_node: None, + unless_node: None, + until_node: None, + switch_node: Some("switch_statement"), + switch_nodes: &[], + case_node: Some("switch_entry"), + case_nodes: &[], + default_node: None, + wildcard_pattern_node: Some("wildcard_pattern"), + try_node: Some("do_statement"), + try_nodes: &[], + catch_node: Some("catch_clause"), + finally_node: None, + else_node: None, + return_node: Some("return_statement"), + throw_node: Some("throw_statement"), + break_node: Some("break_statement"), + continue_node: Some("continue_statement"), + block_node: None, + block_nodes: &["code_block", "class_body"], + labeled_node: Some("labeled_statement"), +}; + +pub static SCALA_CFG: CfgRules = CfgRules { + if_node: Some("if_expression"), + if_nodes: &[], + elif_node: None, + else_clause: None, + else_via_alternative: true, + if_consequent_field: None, + for_nodes: &["for_expression"], + condition_field: Some("condition"), + while_node: Some("while_expression"), + while_nodes: &[], + do_node: None, + infinite_loop_node: None, + unless_node: None, + until_node: None, + switch_node: Some("match_expression"), + switch_nodes: &[], + case_node: Some("case_clause"), + case_nodes: &[], + default_node: None, + wildcard_pattern_node: Some("wildcard"), + try_node: Some("try_expression"), + try_nodes: &[], + catch_node: Some("catch_clause"), + finally_node: Some("finally_clause"), + else_node: None, + return_node: Some("return_expression"), + throw_node: Some("throw_expression"), + break_node: None, + continue_node: None, + block_node: Some("block"), + block_nodes: &["template_body"], + labeled_node: None, +}; + +pub static BASH_CFG: CfgRules = CfgRules { + if_node: Some("if_statement"), + if_nodes: &[], + elif_node: Some("elif_clause"), + else_clause: Some("else_clause"), + else_via_alternative: false, + if_consequent_field: None, + for_nodes: &["for_statement", "c_style_for_statement"], + condition_field: Some("condition"), + while_node: Some("while_statement"), + while_nodes: &[], + do_node: None, + infinite_loop_node: None, + unless_node: None, + until_node: Some("until_statement"), + switch_node: Some("case_statement"), + switch_nodes: &[], + case_node: Some("case_item"), + case_nodes: &[], + default_node: None, + wildcard_pattern_node: None, + try_node: None, + try_nodes: &[], + catch_node: None, + finally_node: None, + else_node: Some("else_clause"), + return_node: Some("return_statement"), + throw_node: None, + break_node: Some("break_statement"), + continue_node: Some("continue_statement"), + block_node: Some("compound_statement"), + block_nodes: &[], + labeled_node: None, +}; + /// Get CFG rules for a language ID. pub fn get_cfg_rules(lang_id: &str) -> Option<&'static CfgRules> { match lang_id { @@ -341,6 +551,12 @@ pub fn get_cfg_rules(lang_id: &str) -> Option<&'static CfgRules> { "csharp" => Some(&CSHARP_CFG), "ruby" => Some(&RUBY_CFG), "php" => Some(&PHP_CFG), + "c" => Some(&C_CFG), + "cpp" => Some(&CPP_CFG), + "kotlin" => Some(&KOTLIN_CFG), + "swift" => Some(&SWIFT_CFG), + "scala" => Some(&SCALA_CFG), + "bash" => Some(&BASH_CFG), _ => None, } } diff --git a/crates/codegraph-core/src/complexity.rs b/crates/codegraph-core/src/complexity.rs index ce81e2b7..3f5e4167 100644 --- a/crates/codegraph-core/src/complexity.rs +++ b/crates/codegraph-core/src/complexity.rs @@ -11,7 +11,7 @@ pub struct LangRules { pub branch_nodes: &'static [&'static str], pub case_nodes: &'static [&'static str], pub logical_operators: &'static [&'static str], - pub logical_node_type: &'static str, + pub logical_node_types: &'static [&'static str], pub optional_chain_type: Option<&'static str>, pub nesting_nodes: &'static [&'static str], pub function_nodes: &'static [&'static str], @@ -59,7 +59,7 @@ pub static JS_TS_RULES: LangRules = LangRules { ], case_nodes: &["switch_case"], logical_operators: &["&&", "||", "??"], - logical_node_type: "binary_expression", + logical_node_types: &["binary_expression"], optional_chain_type: Some("optional_chain_expression"), nesting_nodes: &[ "if_statement", @@ -99,7 +99,7 @@ pub static PYTHON_RULES: LangRules = LangRules { ], case_nodes: &["case_clause"], logical_operators: &["and", "or"], - logical_node_type: "boolean_operator", + logical_node_types: &["boolean_operator"], optional_chain_type: None, nesting_nodes: &[ "if_statement", @@ -131,7 +131,7 @@ pub static GO_RULES: LangRules = LangRules { "communication_case", ], logical_operators: &["&&", "||"], - logical_node_type: "binary_expression", + logical_node_types: &["binary_expression"], optional_chain_type: None, nesting_nodes: &[ "if_statement", @@ -168,7 +168,7 @@ pub static RUST_LANG_RULES: LangRules = LangRules { ], case_nodes: &["match_arm"], logical_operators: &["&&", "||"], - logical_node_type: "binary_expression", + logical_node_types: &["binary_expression"], optional_chain_type: None, nesting_nodes: &[ "if_expression", @@ -200,7 +200,7 @@ pub static JAVA_RULES: LangRules = LangRules { ], case_nodes: &["switch_label"], logical_operators: &["&&", "||"], - logical_node_type: "binary_expression", + logical_node_types: &["binary_expression"], optional_chain_type: None, nesting_nodes: &[ "if_statement", @@ -237,7 +237,7 @@ pub static CSHARP_RULES: LangRules = LangRules { ], case_nodes: &["switch_section"], logical_operators: &["&&", "||", "??"], - logical_node_type: "binary_expression", + logical_node_types: &["binary_expression"], optional_chain_type: Some("conditional_access_expression"), nesting_nodes: &[ "if_statement", @@ -277,7 +277,7 @@ pub static RUBY_RULES: LangRules = LangRules { ], case_nodes: &["when"], logical_operators: &["and", "or", "&&", "||"], - logical_node_type: "binary", + logical_node_types: &["binary"], optional_chain_type: None, nesting_nodes: &[ "if", @@ -312,7 +312,7 @@ pub static PHP_RULES: LangRules = LangRules { ], case_nodes: &["case_statement", "default_statement"], logical_operators: &["&&", "||", "and", "or", "??"], - logical_node_type: "binary_expression", + logical_node_types: &["binary_expression"], optional_chain_type: Some("nullsafe_member_access_expression"), nesting_nodes: &[ "if_statement", @@ -337,6 +337,96 @@ pub static PHP_RULES: LangRules = LangRules { switch_like_nodes: &["switch_statement"], }; +pub static C_RULES: LangRules = LangRules { + branch_nodes: &["if_statement", "for_statement", "while_statement", "do_statement", "case_statement", "conditional_expression"], + case_nodes: &["case_statement"], + logical_operators: &["&&", "||"], + logical_node_types: &["binary_expression"], + optional_chain_type: None, + nesting_nodes: &["if_statement", "for_statement", "while_statement", "do_statement", "conditional_expression"], + function_nodes: &["function_definition"], + if_node_type: Some("if_statement"), + else_node_type: None, + elif_node_type: None, + else_via_alternative: true, + switch_like_nodes: &["switch_statement"], +}; + +pub static CPP_RULES: LangRules = LangRules { + branch_nodes: &["if_statement", "for_statement", "for_range_loop", "while_statement", "do_statement", "case_statement", "conditional_expression", "catch_clause"], + case_nodes: &["case_statement"], + logical_operators: &["&&", "||"], + logical_node_types: &["binary_expression"], + optional_chain_type: None, + nesting_nodes: &["if_statement", "for_statement", "for_range_loop", "while_statement", "do_statement", "catch_clause", "conditional_expression"], + function_nodes: &["function_definition"], + if_node_type: Some("if_statement"), + else_node_type: None, + elif_node_type: None, + else_via_alternative: true, + switch_like_nodes: &["switch_statement"], +}; + +pub static KOTLIN_RULES: LangRules = LangRules { + branch_nodes: &["if_expression", "for_statement", "while_statement", "do_while_statement", "catch_block", "when_expression", "when_entry"], + case_nodes: &["when_entry"], + logical_operators: &["&&", "||"], + logical_node_types: &["conjunction_expression", "disjunction_expression"], + optional_chain_type: Some("safe_navigation"), + nesting_nodes: &["if_expression", "for_statement", "while_statement", "do_while_statement", "catch_block", "when_expression"], + function_nodes: &["function_declaration"], + if_node_type: Some("if_expression"), + else_node_type: None, + elif_node_type: None, + else_via_alternative: true, + switch_like_nodes: &["when_expression"], +}; + +pub static SWIFT_RULES: LangRules = LangRules { + branch_nodes: &["if_statement", "for_in_statement", "while_statement", "repeat_while_statement", "catch_clause", "switch_entry", "ternary_expression", "guard_statement"], + case_nodes: &["switch_entry"], + logical_operators: &["&&", "||"], + logical_node_types: &["binary_expression"], + optional_chain_type: Some("optional_chaining_expression"), + nesting_nodes: &["if_statement", "for_in_statement", "while_statement", "repeat_while_statement", "catch_clause", "ternary_expression", "guard_statement"], + function_nodes: &["function_declaration", "init_declaration"], + if_node_type: Some("if_statement"), + else_node_type: None, + elif_node_type: None, + else_via_alternative: true, + switch_like_nodes: &["switch_statement"], +}; + +pub static SCALA_RULES: LangRules = LangRules { + branch_nodes: &["if_expression", "for_expression", "while_expression", "do_while_expression", "catch_clause", "case_clause", "match_expression"], + case_nodes: &["case_clause"], + logical_operators: &["&&", "||"], + logical_node_types: &["infix_expression"], + optional_chain_type: None, + nesting_nodes: &["if_expression", "for_expression", "while_expression", "do_while_expression", "catch_clause", "match_expression"], + function_nodes: &["function_definition"], + if_node_type: Some("if_expression"), + else_node_type: None, + elif_node_type: None, + else_via_alternative: true, + switch_like_nodes: &["match_expression"], +}; + +pub static BASH_RULES: LangRules = LangRules { + branch_nodes: &["if_statement", "for_statement", "while_statement", "case_statement", "elif_clause"], + case_nodes: &["case_item"], + logical_operators: &["&&", "||"], + logical_node_types: &["binary_expression"], + optional_chain_type: None, + nesting_nodes: &["if_statement", "for_statement", "while_statement", "case_statement"], + function_nodes: &["function_definition"], + if_node_type: Some("if_statement"), + else_node_type: Some("else_clause"), + elif_node_type: Some("elif_clause"), + else_via_alternative: false, + switch_like_nodes: &["case_statement"], +}; + /// Look up complexity rules by language ID (matches `COMPLEXITY_RULES` keys in JS). pub fn lang_rules(lang_id: &str) -> Option<&'static LangRules> { match lang_id { @@ -348,6 +438,12 @@ pub fn lang_rules(lang_id: &str) -> Option<&'static LangRules> { "csharp" => Some(&CSHARP_RULES), "ruby" => Some(&RUBY_RULES), "php" => Some(&PHP_RULES), + "c" => Some(&C_RULES), + "cpp" => Some(&CPP_RULES), + "kotlin" => Some(&KOTLIN_RULES), + "swift" => Some(&SWIFT_RULES), + "scala" => Some(&SCALA_RULES), + "bash" => Some(&BASH_RULES), _ => None, } } @@ -521,7 +617,7 @@ fn handle_logical_op( cognitive: &mut u32, cyclomatic: &mut u32, ) -> bool { - if kind != rules.logical_node_type { + if !rules.logical_node_types.contains(&kind) { return false; } let Some(op_node) = node.child(1) else { return false }; @@ -534,7 +630,7 @@ fn handle_logical_op( // Cognitive: +1 only when operator changes from the previous sibling sequence let same_sequence = node.parent().map_or(false, |parent| { - parent.kind() == rules.logical_node_type + rules.logical_node_types.contains(&parent.kind()) && parent.child(1).map_or(false, |pop| pop.kind() == op) }); if !same_sequence { @@ -808,6 +904,128 @@ pub static PHP_HALSTEAD: HalsteadRules = HalsteadRules { skip_types: &[], }; +pub static C_HALSTEAD: HalsteadRules = HalsteadRules { + operator_leaf_types: &[ + "+", "-", "*", "/", "%", "=", "+=", "-=", "*=", "/=", "%=", "&=", "|=", "^=", "<<=", ">>=", + "==", "!=", "<", ">", "<=", ">=", "&&", "||", "!", + "&", "|", "^", "~", "<<", ">>", "++", "--", + "sizeof", "if", "else", "for", "while", "do", "switch", "case", + "return", "break", "continue", "goto", + ".", "->", ",", ";", ":", "?", + ], + operand_leaf_types: &[ + "identifier", "type_identifier", "field_identifier", "number_literal", "string_literal", + "char_literal", "true", "false", "null", + ], + compound_operators: &["call_expression", "subscript_expression"], + skip_types: &[], +}; + +pub static CPP_HALSTEAD: HalsteadRules = HalsteadRules { + operator_leaf_types: &[ + "+", "-", "*", "/", "%", "=", "+=", "-=", "*=", "/=", "%=", "&=", "|=", "^=", "<<=", ">>=", + "==", "!=", "<", ">", "<=", ">=", "&&", "||", "!", + "&", "|", "^", "~", "<<", ">>", "++", "--", + "sizeof", "new", "delete", "throw", + "if", "else", "for", "while", "do", "switch", "case", + "return", "break", "continue", + "try", "catch", + ".", "->", "::", ",", ";", ":", "?", + ], + operand_leaf_types: &[ + "identifier", "type_identifier", "field_identifier", "namespace_identifier", + "number_literal", "string_literal", "raw_string_literal", "char_literal", + "true", "false", "nullptr", "this", + ], + compound_operators: &["call_expression", "subscript_expression", "new_expression"], + skip_types: &["template_argument_list", "template_parameter_list"], +}; + +pub static KOTLIN_HALSTEAD: HalsteadRules = HalsteadRules { + operator_leaf_types: &[ + "+", "-", "*", "/", "%", "=", "+=", "-=", "*=", "/=", "%=", + "==", "!=", "<", ">", "<=", ">=", "===", "!==", + "&&", "||", "!", + "++", "--", "..", "?:", "?.", + "is", "as", "as?", "in", "!in", + "if", "else", "for", "while", "do", "when", + "return", "throw", "break", "continue", + "try", "catch", "finally", + ".", ",", ";", ":", "?", "->", + ], + operand_leaf_types: &[ + "simple_identifier", "type_identifier", + "integer_literal", "long_literal", "real_literal", "hex_literal", "bin_literal", + "string_literal", "character_literal", + "true", "false", "null", "this", "super", + ], + compound_operators: &["call_expression", "indexing_expression"], + skip_types: &["type_arguments", "type_parameters"], +}; + +pub static SWIFT_HALSTEAD: HalsteadRules = HalsteadRules { + operator_leaf_types: &[ + "+", "-", "*", "/", "%", "=", "+=", "-=", "*=", "/=", "%=", + "==", "!=", "<", ">", "<=", ">=", "===", "!==", + "&&", "||", "!", + "?", "??", "...", "..<", + "is", "as", "as?", "as!", + "if", "else", "for", "while", "repeat", "switch", "guard", + "return", "throw", "break", "continue", + "try", "catch", + ".", ",", ";", ":", "->", + ], + operand_leaf_types: &[ + "simple_identifier", "type_identifier", + "integer_literal", "real_literal", "hex_literal", "oct_literal", "bin_literal", + "string_literal", "true", "false", "nil", "self", "super", + ], + compound_operators: &["call_expression", "subscript_expression"], + skip_types: &["type_arguments", "type_parameters"], +}; + +pub static SCALA_HALSTEAD: HalsteadRules = HalsteadRules { + operator_leaf_types: &[ + "+", "-", "*", "/", "%", "=", "+=", "-=", "*=", "/=", "%=", + "==", "!=", "<", ">", "<=", ">=", + "&&", "||", "!", + "::", "++", ":+", "+:", + "if", "else", "for", "while", "do", "match", "case", + "return", "throw", "yield", + "try", "catch", "finally", + ".", ",", ";", ":", "=>", "<-", + ], + operand_leaf_types: &[ + "identifier", "type_identifier", + "integer_literal", "floating_point_literal", + "string_literal", "character_literal", "symbol_literal", + "true", "false", "null", "this", "super", + ], + compound_operators: &["call_expression", "field_expression"], + skip_types: &["type_arguments", "type_parameters"], +}; + +pub static BASH_HALSTEAD: HalsteadRules = HalsteadRules { + operator_leaf_types: &[ + "=", "==", "!=", "-eq", "-ne", "-lt", "-gt", "-le", "-ge", + "-z", "-n", "-f", "-d", "-e", "-r", "-w", "-x", + "&&", "||", "!", + "|", ">>", ">", "<", "<<", + "if", "then", "else", "elif", "fi", + "for", "while", "until", "do", "done", + "case", "esac", "in", + "return", "exit", "break", "continue", + ";", ";;", + ], + operand_leaf_types: &[ + "word", "variable_name", "string", "number", + "raw_string", "simple_expansion", "expansion", + "command_name", + ], + compound_operators: &["command", "command_substitution", "pipeline"], + skip_types: &[], +}; + /// Look up Halstead rules by language ID. pub fn halstead_rules(lang_id: &str) -> Option<&'static HalsteadRules> { match lang_id { @@ -819,6 +1037,12 @@ pub fn halstead_rules(lang_id: &str) -> Option<&'static HalsteadRules> { "csharp" => Some(&CSHARP_HALSTEAD), "ruby" => Some(&RUBY_HALSTEAD), "php" => Some(&PHP_HALSTEAD), + "c" => Some(&C_HALSTEAD), + "cpp" => Some(&CPP_HALSTEAD), + "kotlin" => Some(&KOTLIN_HALSTEAD), + "swift" => Some(&SWIFT_HALSTEAD), + "scala" => Some(&SCALA_HALSTEAD), + "bash" => Some(&BASH_HALSTEAD), _ => None, } } @@ -831,6 +1055,11 @@ pub fn comment_prefixes(lang_id: &str) -> &'static [&'static str] { } "python" | "ruby" => &["#"], "php" => &["//", "#", "/*", "*", "*/"], + "c" | "cpp" => &["//", "/*"], + "kotlin" => &["//", "/*"], + "swift" => &["//", "/*"], + "scala" => &["//", "/*"], + "bash" => &["#"], _ => &["//", "/*", "*", "*/"], } } diff --git a/crates/codegraph-core/src/extractors/bash.rs b/crates/codegraph-core/src/extractors/bash.rs new file mode 100644 index 00000000..9d98ccd7 --- /dev/null +++ b/crates/codegraph-core/src/extractors/bash.rs @@ -0,0 +1,119 @@ +use tree_sitter::{Node, Tree}; +use crate::cfg::build_function_cfg; +use crate::complexity::compute_all_metrics; +use crate::types::*; +use super::helpers::*; +use super::SymbolExtractor; + +pub struct BashExtractor; + +impl SymbolExtractor for BashExtractor { + fn extract(&self, tree: &Tree, source: &[u8], file_path: &str) -> FileSymbols { + let mut symbols = FileSymbols::new(file_path.to_string()); + walk_tree(&tree.root_node(), source, &mut symbols, match_bash_node); + walk_ast_nodes_with_config(&tree.root_node(), source, &mut symbols.ast_nodes, &BASH_AST_CONFIG); + symbols + } +} + +fn match_bash_node(node: &Node, source: &[u8], symbols: &mut FileSymbols, _depth: usize) { + match node.kind() { + "function_definition" => { + if let Some(name_node) = node.child_by_field_name("name") { + let name = node_text(&name_node, source).to_string(); + symbols.definitions.push(Definition { + name, + kind: "function".to_string(), + line: start_line(node), + end_line: Some(end_line(node)), + decorators: None, + complexity: compute_all_metrics(node, source, "bash"), + cfg: build_function_cfg(node, "bash", source), + children: None, + }); + } + } + + "command" => { + if let Some(cmd_name) = find_child(node, "command_name") { + let name = node_text(&cmd_name, source); + match name { + "source" | "." => { + // Treat as import — the argument is the sourced file + // Get the first argument after the command name + for i in 0..node.child_count() { + if let Some(child) = node.child(i) { + if child.kind() == "word" || child.kind() == "string" || child.kind() == "raw_string" { + let path = node_text(&child, source) + .trim_matches(|c| c == '"' || c == '\'') + .to_string(); + if !path.is_empty() { + let last = path.split('/').last().unwrap_or(&path).to_string(); + let mut imp = Import::new(path, vec![last], start_line(node)); + imp.bash_source = Some(true); + symbols.imports.push(imp); + } + break; + } + } + } + } + _ => { + symbols.calls.push(Call { + name: name.to_string(), + line: start_line(node), + dynamic: None, + receiver: None, + }); + } + } + } + } + + _ => {} + } +} + +#[cfg(test)] +mod tests { + use super::*; + use tree_sitter::Parser; + + fn parse_bash(code: &str) -> FileSymbols { + let mut parser = Parser::new(); + parser + .set_language(&tree_sitter_bash::LANGUAGE.into()) + .unwrap(); + let tree = parser.parse(code.as_bytes(), None).unwrap(); + BashExtractor.extract(&tree, code.as_bytes(), "test.sh") + } + + #[test] + fn extracts_function() { + let s = parse_bash("function greet() { echo hello; }"); + let greet = s.definitions.iter().find(|d| d.name == "greet").unwrap(); + assert_eq!(greet.kind, "function"); + } + + #[test] + fn extracts_function_alt_syntax() { + let s = parse_bash("greet() { echo hello; }"); + let greet = s.definitions.iter().find(|d| d.name == "greet").unwrap(); + assert_eq!(greet.kind, "function"); + } + + #[test] + fn extracts_source_import() { + let s = parse_bash("source ./utils.sh"); + assert_eq!(s.imports.len(), 1); + assert_eq!(s.imports[0].path, "./utils.sh"); + assert!(s.imports[0].bash_source.unwrap()); + } + + #[test] + fn extracts_command_calls() { + let s = parse_bash("function main() { ls -la; grep foo bar; }"); + assert!(s.calls.iter().any(|c| c.name == "ls")); + assert!(s.calls.iter().any(|c| c.name == "grep")); + } +} diff --git a/crates/codegraph-core/src/extractors/c.rs b/crates/codegraph-core/src/extractors/c.rs new file mode 100644 index 00000000..38411579 --- /dev/null +++ b/crates/codegraph-core/src/extractors/c.rs @@ -0,0 +1,383 @@ +use tree_sitter::{Node, Tree}; +use crate::cfg::build_function_cfg; +use crate::complexity::compute_all_metrics; +use crate::types::*; +use super::helpers::*; +use super::SymbolExtractor; + +pub struct CExtractor; + +impl SymbolExtractor for CExtractor { + fn extract(&self, tree: &Tree, source: &[u8], file_path: &str) -> FileSymbols { + let mut symbols = FileSymbols::new(file_path.to_string()); + walk_tree(&tree.root_node(), source, &mut symbols, match_c_node); + walk_ast_nodes_with_config(&tree.root_node(), source, &mut symbols.ast_nodes, &C_AST_CONFIG); + walk_tree(&tree.root_node(), source, &mut symbols, match_c_type_map); + symbols + } +} + +// ── Type inference helpers ────────────────────────────────────────────────── + +fn match_c_type_map(node: &Node, source: &[u8], symbols: &mut FileSymbols, _depth: usize) { + match node.kind() { + "declaration" => { + if let Some(type_node) = node.child_by_field_name("type") { + let type_name = node_text(&type_node, source); + for i in 0..node.child_count() { + if let Some(child) = node.child(i) { + if child.kind() == "init_declarator" || child.kind() == "identifier" { + let name_node = if child.kind() == "init_declarator" { + child.child_by_field_name("declarator") + } else { + Some(child) + }; + if let Some(name_node) = name_node { + // Unwrap pointer_declarator chains + let final_name = unwrap_declarator(&name_node, source); + if !final_name.is_empty() { + symbols.type_map.push(TypeMapEntry { + name: final_name, + type_name: type_name.to_string(), + }); + } + } + } + } + } + } + } + "parameter_declaration" => { + if let Some(type_node) = node.child_by_field_name("type") { + if let Some(decl) = node.child_by_field_name("declarator") { + let name = unwrap_declarator(&decl, source); + if !name.is_empty() { + symbols.type_map.push(TypeMapEntry { + name, + type_name: node_text(&type_node, source).to_string(), + }); + } + } + } + } + _ => {} + } +} + +/// Walk pointer_declarator / array_declarator chains to reach the identifier. +fn unwrap_declarator(node: &Node, source: &[u8]) -> String { + let mut current = *node; + loop { + match current.kind() { + "pointer_declarator" | "array_declarator" | "parenthesized_declarator" => { + if let Some(inner) = current.child_by_field_name("declarator") { + current = inner; + } else { + break; + } + } + "identifier" => return node_text(¤t, source).to_string(), + _ => break, + } + } + node_text(¤t, source).to_string() +} + +/// Extract function name from a C function_definition declarator chain. +fn extract_c_function_name(node: &Node, source: &[u8]) -> Option { + let declarator = node.child_by_field_name("declarator")?; + // declarator is typically function_declarator + let inner = if declarator.kind() == "function_declarator" { + declarator.child_by_field_name("declarator") + } else if declarator.kind() == "pointer_declarator" { + // e.g. `int *func()` + let fd = find_child(&declarator, "function_declarator")?; + fd.child_by_field_name("declarator") + } else { + Some(declarator) + }; + inner.map(|n| unwrap_declarator(&n, source)) +} + +/// Extract parameters from a function_definition. +fn extract_c_parameters(node: &Node, source: &[u8]) -> Vec { + let mut params = Vec::new(); + let declarator = match node.child_by_field_name("declarator") { + Some(d) => d, + None => return params, + }; + // Find the function_declarator (may be nested under pointer_declarator) + let func_decl = if declarator.kind() == "function_declarator" { + Some(declarator) + } else { + find_child(&declarator, "function_declarator") + }; + if let Some(func_decl) = func_decl { + if let Some(param_list) = func_decl.child_by_field_name("parameters") { + for i in 0..param_list.child_count() { + if let Some(child) = param_list.child(i) { + if child.kind() == "parameter_declaration" { + if let Some(decl) = child.child_by_field_name("declarator") { + let name = unwrap_declarator(&decl, source); + if !name.is_empty() { + params.push(child_def(name, "parameter", start_line(&child))); + } + } + } + } + } + } + } + params +} + +/// Extract struct/union fields. +fn extract_c_fields(body: &Node, source: &[u8]) -> Vec { + let mut fields = Vec::new(); + for i in 0..body.child_count() { + if let Some(child) = body.child(i) { + if child.kind() == "field_declaration" { + if let Some(decl) = child.child_by_field_name("declarator") { + let name = unwrap_declarator(&decl, source); + if !name.is_empty() { + fields.push(child_def(name, "property", start_line(&child))); + } + } + } + } + } + fields +} + +/// Extract enum constants from enumerator_list. +fn extract_c_enum_constants(node: &Node, source: &[u8]) -> Vec { + let mut constants = Vec::new(); + if let Some(body) = node.child_by_field_name("body") { + for i in 0..body.child_count() { + if let Some(child) = body.child(i) { + if child.kind() == "enumerator" { + if let Some(name_node) = child.child_by_field_name("name") { + constants.push(child_def( + node_text(&name_node, source).to_string(), + "constant", + start_line(&child), + )); + } + } + } + } + } + constants +} + +fn match_c_node(node: &Node, source: &[u8], symbols: &mut FileSymbols, _depth: usize) { + match node.kind() { + "function_definition" => { + if let Some(name) = extract_c_function_name(node, source) { + let children = extract_c_parameters(node, source); + symbols.definitions.push(Definition { + name, + kind: "function".to_string(), + line: start_line(node), + end_line: Some(end_line(node)), + decorators: None, + complexity: compute_all_metrics(node, source, "c"), + cfg: build_function_cfg(node, "c", source), + children: opt_children(children), + }); + } + } + + "struct_specifier" => { + if let Some(name_node) = node.child_by_field_name("name") { + let struct_name = node_text(&name_node, source).to_string(); + let children = node.child_by_field_name("body") + .map(|body| extract_c_fields(&body, source)) + .unwrap_or_default(); + symbols.definitions.push(Definition { + name: struct_name, + kind: "struct".to_string(), + line: start_line(node), + end_line: Some(end_line(node)), + decorators: None, + complexity: None, + cfg: None, + children: opt_children(children), + }); + } + } + + "union_specifier" => { + if let Some(name_node) = node.child_by_field_name("name") { + let children = node.child_by_field_name("body") + .map(|body| extract_c_fields(&body, source)) + .unwrap_or_default(); + symbols.definitions.push(Definition { + name: node_text(&name_node, source).to_string(), + kind: "struct".to_string(), + line: start_line(node), + end_line: Some(end_line(node)), + decorators: None, + complexity: None, + cfg: None, + children: opt_children(children), + }); + } + } + + "enum_specifier" => { + if let Some(name_node) = node.child_by_field_name("name") { + let children = extract_c_enum_constants(node, source); + symbols.definitions.push(Definition { + name: node_text(&name_node, source).to_string(), + kind: "enum".to_string(), + line: start_line(node), + end_line: Some(end_line(node)), + decorators: None, + complexity: None, + cfg: None, + children: opt_children(children), + }); + } + } + + "type_definition" => { + // typedef — the last type_identifier or identifier child is the alias name + let mut alias_name = None; + for i in (0..node.child_count()).rev() { + if let Some(child) = node.child(i) { + match child.kind() { + "type_identifier" | "identifier" | "primitive_type" => { + alias_name = Some(node_text(&child, source).to_string()); + break; + } + _ => {} + } + } + } + if let Some(name) = alias_name { + symbols.definitions.push(Definition { + name, + kind: "type".to_string(), + line: start_line(node), + end_line: Some(end_line(node)), + decorators: None, + complexity: None, + cfg: None, + children: None, + }); + } + } + + "preproc_include" => { + if let Some(path_node) = node.child_by_field_name("path") { + let raw = node_text(&path_node, source); + let path = raw.trim_matches(|c| c == '"' || c == '<' || c == '>'); + if !path.is_empty() { + let last = path.split('/').last().unwrap_or(path); + let name = last.strip_suffix(".h").unwrap_or(last); + let mut imp = Import::new(path.to_string(), vec![name.to_string()], start_line(node)); + imp.c_include = Some(true); + symbols.imports.push(imp); + } + } + } + + "call_expression" => { + if let Some(fn_node) = node.child_by_field_name("function") { + match fn_node.kind() { + "identifier" => { + symbols.calls.push(Call { + name: node_text(&fn_node, source).to_string(), + line: start_line(node), + dynamic: None, + receiver: None, + }); + } + "field_expression" => { + let name = fn_node.child_by_field_name("field") + .map(|n| node_text(&n, source).to_string()) + .unwrap_or_else(|| node_text(&fn_node, source).to_string()); + let receiver = fn_node.child_by_field_name("argument") + .map(|n| node_text(&n, source).to_string()); + symbols.calls.push(Call { + name, + line: start_line(node), + dynamic: None, + receiver, + }); + } + _ => { + symbols.calls.push(Call { + name: node_text(&fn_node, source).to_string(), + line: start_line(node), + dynamic: None, + receiver: None, + }); + } + } + } + } + + _ => {} + } +} + +#[cfg(test)] +mod tests { + use super::*; + use tree_sitter::Parser; + + fn parse_c(code: &str) -> FileSymbols { + let mut parser = Parser::new(); + parser + .set_language(&tree_sitter_c::LANGUAGE.into()) + .unwrap(); + let tree = parser.parse(code.as_bytes(), None).unwrap(); + CExtractor.extract(&tree, code.as_bytes(), "test.c") + } + + #[test] + fn extracts_function() { + let s = parse_c("int main(int argc, char *argv[]) { return 0; }"); + let main = s.definitions.iter().find(|d| d.name == "main").unwrap(); + assert_eq!(main.kind, "function"); + let children = main.children.as_ref().unwrap(); + assert_eq!(children.len(), 2); + assert_eq!(children[0].name, "argc"); + } + + #[test] + fn extracts_struct() { + let s = parse_c("struct Point { int x; int y; };"); + let point = s.definitions.iter().find(|d| d.name == "Point").unwrap(); + assert_eq!(point.kind, "struct"); + let children = point.children.as_ref().unwrap(); + assert_eq!(children.len(), 2); + } + + #[test] + fn extracts_enum() { + let s = parse_c("enum Color { RED, GREEN, BLUE };"); + let e = s.definitions.iter().find(|d| d.name == "Color").unwrap(); + assert_eq!(e.kind, "enum"); + let children = e.children.as_ref().unwrap(); + assert_eq!(children.len(), 3); + assert_eq!(children[0].name, "RED"); + } + + #[test] + fn extracts_include() { + let s = parse_c("#include \n#include \"mylib.h\""); + assert_eq!(s.imports.len(), 2); + assert_eq!(s.imports[0].path, "stdio.h"); + assert!(s.imports[0].c_include.unwrap()); + } + + #[test] + fn extracts_call() { + let s = parse_c("void f() { printf(\"hello\"); }"); + let call = s.calls.iter().find(|c| c.name == "printf").unwrap(); + assert_eq!(call.name, "printf"); + } +} diff --git a/crates/codegraph-core/src/extractors/cpp.rs b/crates/codegraph-core/src/extractors/cpp.rs new file mode 100644 index 00000000..8fb30108 --- /dev/null +++ b/crates/codegraph-core/src/extractors/cpp.rs @@ -0,0 +1,439 @@ +use tree_sitter::{Node, Tree}; +use crate::cfg::build_function_cfg; +use crate::complexity::compute_all_metrics; +use crate::types::*; +use super::helpers::*; +use super::SymbolExtractor; + +pub struct CppExtractor; + +impl SymbolExtractor for CppExtractor { + fn extract(&self, tree: &Tree, source: &[u8], file_path: &str) -> FileSymbols { + let mut symbols = FileSymbols::new(file_path.to_string()); + walk_tree(&tree.root_node(), source, &mut symbols, match_cpp_node); + walk_ast_nodes_with_config(&tree.root_node(), source, &mut symbols.ast_nodes, &CPP_AST_CONFIG); + walk_tree(&tree.root_node(), source, &mut symbols, match_cpp_type_map); + symbols + } +} + +// ── Type inference ────────────────────────────────────────────────────────── + +fn match_cpp_type_map(node: &Node, source: &[u8], symbols: &mut FileSymbols, _depth: usize) { + match node.kind() { + "declaration" => { + if let Some(type_node) = node.child_by_field_name("type") { + let type_name = node_text(&type_node, source); + for i in 0..node.child_count() { + if let Some(child) = node.child(i) { + if child.kind() == "init_declarator" || child.kind() == "identifier" { + let name_node = if child.kind() == "init_declarator" { + child.child_by_field_name("declarator") + } else { + Some(child) + }; + if let Some(name_node) = name_node { + let final_name = unwrap_cpp_declarator(&name_node, source); + if !final_name.is_empty() { + symbols.type_map.push(TypeMapEntry { + name: final_name, + type_name: type_name.to_string(), + }); + } + } + } + } + } + } + } + "parameter_declaration" => { + if let Some(type_node) = node.child_by_field_name("type") { + if let Some(decl) = node.child_by_field_name("declarator") { + let name = unwrap_cpp_declarator(&decl, source); + if !name.is_empty() { + symbols.type_map.push(TypeMapEntry { + name, + type_name: node_text(&type_node, source).to_string(), + }); + } + } + } + } + _ => {} + } +} + +fn unwrap_cpp_declarator(node: &Node, source: &[u8]) -> String { + let mut current = *node; + loop { + match current.kind() { + "pointer_declarator" | "reference_declarator" | "array_declarator" + | "parenthesized_declarator" => { + if let Some(inner) = current.child_by_field_name("declarator") { + current = inner; + } else { + break; + } + } + "identifier" | "field_identifier" => return node_text(¤t, source).to_string(), + _ => break, + } + } + node_text(¤t, source).to_string() +} + +fn extract_cpp_function_name(node: &Node, source: &[u8]) -> Option { + let declarator = node.child_by_field_name("declarator")?; + extract_cpp_func_name_from_declarator(&declarator, source) +} + +fn extract_cpp_func_name_from_declarator(declarator: &Node, source: &[u8]) -> Option { + match declarator.kind() { + "function_declarator" => { + let inner = declarator.child_by_field_name("declarator")?; + Some(unwrap_cpp_declarator(&inner, source)) + } + "pointer_declarator" | "reference_declarator" => { + let inner = find_child(declarator, "function_declarator")?; + let name_node = inner.child_by_field_name("declarator")?; + Some(unwrap_cpp_declarator(&name_node, source)) + } + _ => Some(unwrap_cpp_declarator(declarator, source)), + } +} + +fn extract_cpp_parameters(node: &Node, source: &[u8]) -> Vec { + let mut params = Vec::new(); + let declarator = match node.child_by_field_name("declarator") { + Some(d) => d, + None => return params, + }; + let func_decl = if declarator.kind() == "function_declarator" { + Some(declarator) + } else { + find_child(&declarator, "function_declarator") + }; + if let Some(func_decl) = func_decl { + if let Some(param_list) = func_decl.child_by_field_name("parameters") { + for i in 0..param_list.child_count() { + if let Some(child) = param_list.child(i) { + if child.kind() == "parameter_declaration" || child.kind() == "optional_parameter_declaration" { + if let Some(decl) = child.child_by_field_name("declarator") { + let name = unwrap_cpp_declarator(&decl, source); + if !name.is_empty() { + params.push(child_def(name, "parameter", start_line(&child))); + } + } + } + } + } + } + } + params +} + +fn extract_cpp_fields(body: &Node, source: &[u8]) -> Vec { + let mut fields = Vec::new(); + for i in 0..body.child_count() { + if let Some(child) = body.child(i) { + if child.kind() == "field_declaration" { + if let Some(decl) = child.child_by_field_name("declarator") { + let name = unwrap_cpp_declarator(&decl, source); + if !name.is_empty() { + fields.push(child_def(name, "property", start_line(&child))); + } + } + } + } + } + fields +} + +fn extract_cpp_enum_constants(node: &Node, source: &[u8]) -> Vec { + let mut constants = Vec::new(); + if let Some(body) = node.child_by_field_name("body") { + for i in 0..body.child_count() { + if let Some(child) = body.child(i) { + if child.kind() == "enumerator" { + if let Some(name_node) = child.child_by_field_name("name") { + constants.push(child_def( + node_text(&name_node, source).to_string(), + "constant", + start_line(&child), + )); + } + } + } + } + } + constants +} + +fn find_cpp_parent_class<'a>(node: &Node<'a>, source: &[u8]) -> Option { + let mut current = node.parent(); + while let Some(parent) = current { + match parent.kind() { + "class_specifier" | "struct_specifier" => { + return parent.child_by_field_name("name") + .map(|n| node_text(&n, source).to_string()); + } + _ => {} + } + current = parent.parent(); + } + None +} + +fn extract_cpp_base_classes(node: &Node, source: &[u8], class_name: &str, symbols: &mut FileSymbols) { + for i in 0..node.child_count() { + if let Some(child) = node.child(i) { + if child.kind() == "base_class_clause" { + for j in 0..child.child_count() { + if let Some(base) = child.child(j) { + match base.kind() { + "type_identifier" | "qualified_identifier" | "scoped_type_identifier" => { + symbols.classes.push(ClassRelation { + name: class_name.to_string(), + extends: Some(node_text(&base, source).to_string()), + implements: None, + line: start_line(node), + }); + } + _ => {} + } + } + } + } + } + } +} + +fn match_cpp_node(node: &Node, source: &[u8], symbols: &mut FileSymbols, _depth: usize) { + match node.kind() { + "function_definition" => { + if let Some(name) = extract_cpp_function_name(node, source) { + let parent_class = find_cpp_parent_class(node, source); + let full_name = match &parent_class { + Some(cls) => format!("{}.{}", cls, name), + None => name, + }; + let kind = if parent_class.is_some() { "method" } else { "function" }; + let children = extract_cpp_parameters(node, source); + symbols.definitions.push(Definition { + name: full_name, + kind: kind.to_string(), + line: start_line(node), + end_line: Some(end_line(node)), + decorators: None, + complexity: compute_all_metrics(node, source, "cpp"), + cfg: build_function_cfg(node, "cpp", source), + children: opt_children(children), + }); + } + } + + "class_specifier" => { + if let Some(name_node) = node.child_by_field_name("name") { + let class_name = node_text(&name_node, source).to_string(); + let children = node.child_by_field_name("body") + .map(|body| extract_cpp_fields(&body, source)) + .unwrap_or_default(); + symbols.definitions.push(Definition { + name: class_name.clone(), + kind: "class".to_string(), + line: start_line(node), + end_line: Some(end_line(node)), + decorators: None, + complexity: None, + cfg: None, + children: opt_children(children), + }); + extract_cpp_base_classes(node, source, &class_name, symbols); + } + } + + "struct_specifier" => { + if let Some(name_node) = node.child_by_field_name("name") { + let struct_name = node_text(&name_node, source).to_string(); + let children = node.child_by_field_name("body") + .map(|body| extract_cpp_fields(&body, source)) + .unwrap_or_default(); + symbols.definitions.push(Definition { + name: struct_name.clone(), + kind: "struct".to_string(), + line: start_line(node), + end_line: Some(end_line(node)), + decorators: None, + complexity: None, + cfg: None, + children: opt_children(children), + }); + extract_cpp_base_classes(node, source, &struct_name, symbols); + } + } + + "enum_specifier" => { + if let Some(name_node) = node.child_by_field_name("name") { + let children = extract_cpp_enum_constants(node, source); + symbols.definitions.push(Definition { + name: node_text(&name_node, source).to_string(), + kind: "enum".to_string(), + line: start_line(node), + end_line: Some(end_line(node)), + decorators: None, + complexity: None, + cfg: None, + children: opt_children(children), + }); + } + } + + "namespace_definition" => { + if let Some(name_node) = node.child_by_field_name("name") { + symbols.definitions.push(Definition { + name: node_text(&name_node, source).to_string(), + kind: "namespace".to_string(), + line: start_line(node), + end_line: Some(end_line(node)), + decorators: None, + complexity: None, + cfg: None, + children: None, + }); + } + } + + "type_definition" => { + let mut alias_name = None; + for i in (0..node.child_count()).rev() { + if let Some(child) = node.child(i) { + match child.kind() { + "type_identifier" | "identifier" | "primitive_type" => { + alias_name = Some(node_text(&child, source).to_string()); + break; + } + _ => {} + } + } + } + if let Some(name) = alias_name { + symbols.definitions.push(Definition { + name, + kind: "type".to_string(), + line: start_line(node), + end_line: Some(end_line(node)), + decorators: None, + complexity: None, + cfg: None, + children: None, + }); + } + } + + "preproc_include" => { + if let Some(path_node) = node.child_by_field_name("path") { + let raw = node_text(&path_node, source); + let path = raw.trim_matches(|c| c == '"' || c == '<' || c == '>'); + if !path.is_empty() { + let last = path.split('/').last().unwrap_or(path); + let name = last.strip_suffix(".h") + .or_else(|| last.strip_suffix(".hpp")) + .unwrap_or(last); + let mut imp = Import::new(path.to_string(), vec![name.to_string()], start_line(node)); + imp.c_include = Some(true); + symbols.imports.push(imp); + } + } + } + + "call_expression" => { + if let Some(fn_node) = node.child_by_field_name("function") { + match fn_node.kind() { + "identifier" | "qualified_identifier" | "scoped_identifier" => { + symbols.calls.push(Call { + name: node_text(&fn_node, source).to_string(), + line: start_line(node), + dynamic: None, + receiver: None, + }); + } + "field_expression" => { + let name = fn_node.child_by_field_name("field") + .map(|n| node_text(&n, source).to_string()) + .unwrap_or_else(|| node_text(&fn_node, source).to_string()); + let receiver = fn_node.child_by_field_name("argument") + .map(|n| node_text(&n, source).to_string()); + symbols.calls.push(Call { + name, + line: start_line(node), + dynamic: None, + receiver, + }); + } + _ => { + symbols.calls.push(Call { + name: node_text(&fn_node, source).to_string(), + line: start_line(node), + dynamic: None, + receiver: None, + }); + } + } + } + } + + _ => {} + } +} + +#[cfg(test)] +mod tests { + use super::*; + use tree_sitter::Parser; + + fn parse_cpp(code: &str) -> FileSymbols { + let mut parser = Parser::new(); + parser + .set_language(&tree_sitter_cpp::LANGUAGE.into()) + .unwrap(); + let tree = parser.parse(code.as_bytes(), None).unwrap(); + CppExtractor.extract(&tree, code.as_bytes(), "test.cpp") + } + + #[test] + fn extracts_function() { + let s = parse_cpp("int main(int argc) { return 0; }"); + let main = s.definitions.iter().find(|d| d.name == "main").unwrap(); + assert_eq!(main.kind, "function"); + } + + #[test] + fn extracts_class_with_method() { + let s = parse_cpp("class Foo { public: void bar() {} };"); + let foo = s.definitions.iter().find(|d| d.name == "Foo").unwrap(); + assert_eq!(foo.kind, "class"); + let bar = s.definitions.iter().find(|d| d.name == "Foo.bar").unwrap(); + assert_eq!(bar.kind, "method"); + } + + #[test] + fn extracts_namespace() { + let s = parse_cpp("namespace myns { int x; }"); + let ns = s.definitions.iter().find(|d| d.name == "myns").unwrap(); + assert_eq!(ns.kind, "namespace"); + } + + #[test] + fn extracts_inheritance() { + let s = parse_cpp("class Base {}; class Derived : public Base {};"); + let rel = s.classes.iter().find(|c| c.name == "Derived").unwrap(); + assert_eq!(rel.extends.as_deref(), Some("Base")); + } + + #[test] + fn extracts_include() { + let s = parse_cpp("#include \n#include \"mylib.hpp\""); + assert_eq!(s.imports.len(), 2); + assert!(s.imports[0].c_include.unwrap()); + } +} diff --git a/crates/codegraph-core/src/extractors/helpers.rs b/crates/codegraph-core/src/extractors/helpers.rs index 1dccae00..441e1b50 100644 --- a/crates/codegraph-core/src/extractors/helpers.rs +++ b/crates/codegraph-core/src/extractors/helpers.rs @@ -250,6 +250,72 @@ pub const PHP_AST_CONFIG: LangAstConfig = LangAstConfig { string_prefixes: &[], }; +pub const C_AST_CONFIG: LangAstConfig = LangAstConfig { + call_types: &["call_expression"], + new_types: &[], + throw_types: &[], + await_types: &[], + string_types: &["string_literal"], + regex_types: &[], + quote_chars: &['"'], + string_prefixes: &[], +}; + +pub const CPP_AST_CONFIG: LangAstConfig = LangAstConfig { + call_types: &["call_expression"], + new_types: &["new_expression"], + throw_types: &["throw_statement"], + await_types: &["co_await_expression"], + string_types: &["string_literal", "raw_string_literal"], + regex_types: &[], + quote_chars: &['"'], + string_prefixes: &['L', 'u', 'U', 'R'], +}; + +pub const KOTLIN_AST_CONFIG: LangAstConfig = LangAstConfig { + call_types: &["call_expression"], + new_types: &[], + throw_types: &["throw_expression"], + await_types: &[], + string_types: &["string_literal"], + regex_types: &[], + quote_chars: &['"'], + string_prefixes: &[], +}; + +pub const SWIFT_AST_CONFIG: LangAstConfig = LangAstConfig { + call_types: &["call_expression"], + new_types: &[], + throw_types: &["throw_statement"], + await_types: &["await_expression"], + string_types: &["string_literal"], + regex_types: &[], + quote_chars: &['"'], + string_prefixes: &[], +}; + +pub const SCALA_AST_CONFIG: LangAstConfig = LangAstConfig { + call_types: &["call_expression"], + new_types: &["object_creation_expression"], + throw_types: &["throw_expression"], + await_types: &[], + string_types: &["string_literal"], + regex_types: &[], + quote_chars: &['"'], + string_prefixes: &[], +}; + +pub const BASH_AST_CONFIG: LangAstConfig = LangAstConfig { + call_types: &["command", "command_substitution"], + new_types: &[], + throw_types: &[], + await_types: &[], + string_types: &["string", "expansion"], + regex_types: &[], + quote_chars: &['"', '\''], + string_prefixes: &[], +}; + // ── Generic AST node walker ────────────────────────────────────────────────── /// Node types that represent identifiers across languages. diff --git a/crates/codegraph-core/src/extractors/kotlin.rs b/crates/codegraph-core/src/extractors/kotlin.rs new file mode 100644 index 00000000..df5e6841 --- /dev/null +++ b/crates/codegraph-core/src/extractors/kotlin.rs @@ -0,0 +1,398 @@ +use tree_sitter::{Node, Tree}; +use crate::cfg::build_function_cfg; +use crate::complexity::compute_all_metrics; +use crate::types::*; +use super::helpers::*; +use super::SymbolExtractor; + +pub struct KotlinExtractor; + +impl SymbolExtractor for KotlinExtractor { + fn extract(&self, tree: &Tree, source: &[u8], file_path: &str) -> FileSymbols { + let mut symbols = FileSymbols::new(file_path.to_string()); + walk_tree(&tree.root_node(), source, &mut symbols, match_kotlin_node); + walk_ast_nodes_with_config(&tree.root_node(), source, &mut symbols.ast_nodes, &KOTLIN_AST_CONFIG); + walk_tree(&tree.root_node(), source, &mut symbols, match_kotlin_type_map); + symbols + } +} + +// ── Type inference ────────────────────────────────────────────────────────── + +fn match_kotlin_type_map(node: &Node, source: &[u8], symbols: &mut FileSymbols, _depth: usize) { + match node.kind() { + "property_declaration" => { + if let Some(type_node) = node.child_by_field_name("type") { + let type_name = node_text(&type_node, source); + // Name can be in a pattern child or directly via field + let name = node.child_by_field_name("name") + .or_else(|| find_child(node, "simple_identifier")) + .map(|n| node_text(&n, source).to_string()); + if let Some(name) = name { + symbols.type_map.push(TypeMapEntry { + name, + type_name: type_name.to_string(), + }); + } + } + } + "parameter" => { + if let Some(type_node) = node.child_by_field_name("type") { + if let Some(name_node) = node.child_by_field_name("name") + .or_else(|| find_child(node, "simple_identifier")) + { + symbols.type_map.push(TypeMapEntry { + name: node_text(&name_node, source).to_string(), + type_name: node_text(&type_node, source).to_string(), + }); + } + } + } + _ => {} + } +} + +// ── Helpers ────────────────────────────────────────────────────────────────── + +/// Check if a class_declaration has an "interface" keyword child. +fn is_kotlin_interface(node: &Node) -> bool { + for i in 0..node.child_count() { + if let Some(child) = node.child(i) { + if child.kind() == "interface" { + return true; + } + } + } + false +} + +/// Check if a class_declaration has "enum" in its modifiers. +fn is_kotlin_enum(node: &Node) -> bool { + for i in 0..node.child_count() { + if let Some(child) = node.child(i) { + if child.kind() == "modifiers" { + for j in 0..child.child_count() { + if let Some(mod_child) = child.child(j) { + if mod_child.kind() == "enum" { + return true; + } + } + } + } + // Also check direct "enum" keyword child + if child.kind() == "enum" { + return true; + } + } + } + false +} + +fn find_kotlin_parent_class<'a>(node: &Node<'a>, source: &[u8]) -> Option { + let mut current = node.parent(); + while let Some(parent) = current { + match parent.kind() { + "class_declaration" | "object_declaration" => { + return find_child(&parent, "type_identifier") + .map(|n| node_text(&n, source).to_string()); + } + _ => {} + } + current = parent.parent(); + } + None +} + +fn extract_kotlin_parameters(node: &Node, source: &[u8]) -> Vec { + let mut params = Vec::new(); + if let Some(param_list) = node.child_by_field_name("parameters") + .or_else(|| find_child(node, "function_value_parameters")) + { + for i in 0..param_list.child_count() { + if let Some(child) = param_list.child(i) { + if child.kind() == "parameter" { + if let Some(name_node) = child.child_by_field_name("name") + .or_else(|| find_child(&child, "simple_identifier")) + { + params.push(child_def( + node_text(&name_node, source).to_string(), + "parameter", + start_line(&child), + )); + } + } + } + } + } + params +} + +fn extract_kotlin_class_properties(node: &Node, source: &[u8]) -> Vec { + let mut props = Vec::new(); + if let Some(body) = find_child(node, "class_body") { + for i in 0..body.child_count() { + if let Some(child) = body.child(i) { + if child.kind() == "property_declaration" { + let name = child.child_by_field_name("name") + .or_else(|| find_child(&child, "simple_identifier")) + .map(|n| node_text(&n, source).to_string()); + if let Some(name) = name { + props.push(child_def(name, "property", start_line(&child))); + } + } + } + } + } + props +} + +fn extract_kotlin_enum_entries(node: &Node, source: &[u8]) -> Vec { + let mut entries = Vec::new(); + if let Some(body) = find_child(node, "class_body") { + for i in 0..body.child_count() { + if let Some(child) = body.child(i) { + if child.kind() == "enum_entry" { + if let Some(name_node) = find_child(&child, "simple_identifier") { + entries.push(child_def( + node_text(&name_node, source).to_string(), + "constant", + start_line(&child), + )); + } + } + } + } + } + entries +} + +fn extract_kotlin_delegation_specifiers(node: &Node, source: &[u8], class_name: &str, symbols: &mut FileSymbols) { + for i in 0..node.child_count() { + if let Some(child) = node.child(i) { + if child.kind() == "delegation_specifier" { + // constructor_invocation > user_type > type_identifier => extends + if let Some(ctor) = find_child(&child, "constructor_invocation") { + if let Some(user_type) = find_child(&ctor, "user_type") { + if let Some(type_id) = find_child(&user_type, "type_identifier") { + symbols.classes.push(ClassRelation { + name: class_name.to_string(), + extends: Some(node_text(&type_id, source).to_string()), + implements: None, + line: start_line(node), + }); + } + } + } + // user_type > type_identifier => implements (interface) + else if let Some(user_type) = find_child(&child, "user_type") { + if let Some(type_id) = find_child(&user_type, "type_identifier") { + symbols.classes.push(ClassRelation { + name: class_name.to_string(), + extends: None, + implements: Some(node_text(&type_id, source).to_string()), + line: start_line(node), + }); + } + } + } + } + } +} + +fn match_kotlin_node(node: &Node, source: &[u8], symbols: &mut FileSymbols, _depth: usize) { + match node.kind() { + "class_declaration" => { + let name_node = find_child(node, "type_identifier"); + if let Some(name_node) = name_node { + let class_name = node_text(&name_node, source).to_string(); + + if is_kotlin_interface(node) { + symbols.definitions.push(Definition { + name: class_name.clone(), + kind: "interface".to_string(), + line: start_line(node), + end_line: Some(end_line(node)), + decorators: None, + complexity: None, + cfg: None, + children: None, + }); + } else if is_kotlin_enum(node) { + let children = extract_kotlin_enum_entries(node, source); + symbols.definitions.push(Definition { + name: class_name.clone(), + kind: "enum".to_string(), + line: start_line(node), + end_line: Some(end_line(node)), + decorators: None, + complexity: None, + cfg: None, + children: opt_children(children), + }); + } else { + let children = extract_kotlin_class_properties(node, source); + symbols.definitions.push(Definition { + name: class_name.clone(), + kind: "class".to_string(), + line: start_line(node), + end_line: Some(end_line(node)), + decorators: None, + complexity: None, + cfg: None, + children: opt_children(children), + }); + } + + extract_kotlin_delegation_specifiers(node, source, &class_name, symbols); + } + } + + "object_declaration" => { + if let Some(name_node) = find_child(node, "type_identifier") { + let obj_name = node_text(&name_node, source).to_string(); + let children = extract_kotlin_class_properties(node, source); + symbols.definitions.push(Definition { + name: obj_name.clone(), + kind: "class".to_string(), + line: start_line(node), + end_line: Some(end_line(node)), + decorators: None, + complexity: None, + cfg: None, + children: opt_children(children), + }); + extract_kotlin_delegation_specifiers(node, source, &obj_name, symbols); + } + } + + "function_declaration" => { + if let Some(name_node) = find_child(node, "simple_identifier") { + let parent_class = find_kotlin_parent_class(node, source); + let name = node_text(&name_node, source); + let full_name = match &parent_class { + Some(cls) => format!("{}.{}", cls, name), + None => name.to_string(), + }; + let kind = if parent_class.is_some() { "method" } else { "function" }; + let children = extract_kotlin_parameters(node, source); + symbols.definitions.push(Definition { + name: full_name, + kind: kind.to_string(), + line: start_line(node), + end_line: Some(end_line(node)), + decorators: None, + complexity: compute_all_metrics(node, source, "kotlin"), + cfg: build_function_cfg(node, "kotlin", source), + children: opt_children(children), + }); + } + } + + "import_header" => { + if let Some(id_node) = find_child(node, "identifier") { + let path = node_text(&id_node, source).to_string(); + let last = path.split('.').last().unwrap_or("").to_string(); + let mut imp = Import::new(path, vec![last], start_line(node)); + imp.kotlin_import = Some(true); + symbols.imports.push(imp); + } + } + + "call_expression" => { + // function child is the callee + if let Some(fn_node) = node.child_by_field_name("function") + .or_else(|| node.child(0)) + { + match fn_node.kind() { + "simple_identifier" => { + symbols.calls.push(Call { + name: node_text(&fn_node, source).to_string(), + line: start_line(node), + dynamic: None, + receiver: None, + }); + } + "navigation_expression" => { + // obj.method() + let name = fn_node.child_by_field_name("member") + .or_else(|| { + let count = fn_node.child_count(); + if count > 0 { fn_node.child(count - 1) } else { None } + }) + .map(|n| node_text(&n, source).to_string()) + .unwrap_or_else(|| node_text(&fn_node, source).to_string()); + let receiver = fn_node.child(0) + .map(|n| node_text(&n, source).to_string()); + symbols.calls.push(Call { + name, + line: start_line(node), + dynamic: None, + receiver, + }); + } + _ => { + symbols.calls.push(Call { + name: node_text(&fn_node, source).to_string(), + line: start_line(node), + dynamic: None, + receiver: None, + }); + } + } + } + } + + _ => {} + } +} + +#[cfg(test)] +mod tests { + use super::*; + use tree_sitter::Parser; + + fn parse_kotlin(code: &str) -> FileSymbols { + let mut parser = Parser::new(); + parser + .set_language(&crate::parser_registry::LanguageKind::Kotlin.tree_sitter_language()) + .unwrap(); + let tree = parser.parse(code.as_bytes(), None).unwrap(); + KotlinExtractor.extract(&tree, code.as_bytes(), "Test.kt") + } + + #[test] + fn extracts_class() { + let s = parse_kotlin("class Foo { val x: Int = 1 }"); + let foo = s.definitions.iter().find(|d| d.name == "Foo").unwrap(); + assert_eq!(foo.kind, "class"); + } + + #[test] + fn extracts_interface() { + let s = parse_kotlin("interface Baz { fun doIt() }"); + let baz = s.definitions.iter().find(|d| d.name == "Baz").unwrap(); + assert_eq!(baz.kind, "interface"); + } + + #[test] + fn extracts_function() { + let s = parse_kotlin("fun greet(name: String): String { return name }"); + let greet = s.definitions.iter().find(|d| d.name == "greet").unwrap(); + assert_eq!(greet.kind, "function"); + } + + #[test] + fn extracts_import() { + let s = parse_kotlin("import com.example.Foo"); + assert_eq!(s.imports.len(), 1); + assert_eq!(s.imports[0].path, "com.example.Foo"); + assert!(s.imports[0].kotlin_import.unwrap()); + } + + #[test] + fn extracts_object() { + let s = parse_kotlin("object Singleton { val x = 1 }"); + let obj = s.definitions.iter().find(|d| d.name == "Singleton").unwrap(); + assert_eq!(obj.kind, "class"); + } +} diff --git a/crates/codegraph-core/src/extractors/mod.rs b/crates/codegraph-core/src/extractors/mod.rs index e9e2bc41..0a9984db 100644 --- a/crates/codegraph-core/src/extractors/mod.rs +++ b/crates/codegraph-core/src/extractors/mod.rs @@ -1,13 +1,19 @@ +pub mod bash; +pub mod c; +pub mod cpp; pub mod csharp; pub mod go; pub mod hcl; pub mod helpers; pub mod java; pub mod javascript; +pub mod kotlin; pub mod php; pub mod python; pub mod ruby; pub mod rust_lang; +pub mod scala; +pub mod swift; use crate::parser_registry::LanguageKind; use crate::types::FileSymbols; @@ -78,5 +84,23 @@ pub fn extract_symbols_with_opts( LanguageKind::Hcl => { hcl::HclExtractor.extract_with_opts(tree, source, file_path, include_ast_nodes) } + LanguageKind::C => { + c::CExtractor.extract_with_opts(tree, source, file_path, include_ast_nodes) + } + LanguageKind::Cpp => { + cpp::CppExtractor.extract_with_opts(tree, source, file_path, include_ast_nodes) + } + LanguageKind::Kotlin => { + kotlin::KotlinExtractor.extract_with_opts(tree, source, file_path, include_ast_nodes) + } + LanguageKind::Swift => { + swift::SwiftExtractor.extract_with_opts(tree, source, file_path, include_ast_nodes) + } + LanguageKind::Scala => { + scala::ScalaExtractor.extract_with_opts(tree, source, file_path, include_ast_nodes) + } + LanguageKind::Bash => { + bash::BashExtractor.extract_with_opts(tree, source, file_path, include_ast_nodes) + } } } diff --git a/crates/codegraph-core/src/extractors/scala.rs b/crates/codegraph-core/src/extractors/scala.rs new file mode 100644 index 00000000..ddc2faa0 --- /dev/null +++ b/crates/codegraph-core/src/extractors/scala.rs @@ -0,0 +1,384 @@ +use tree_sitter::{Node, Tree}; +use crate::cfg::build_function_cfg; +use crate::complexity::compute_all_metrics; +use crate::types::*; +use super::helpers::*; +use super::SymbolExtractor; + +pub struct ScalaExtractor; + +impl SymbolExtractor for ScalaExtractor { + fn extract(&self, tree: &Tree, source: &[u8], file_path: &str) -> FileSymbols { + let mut symbols = FileSymbols::new(file_path.to_string()); + walk_tree(&tree.root_node(), source, &mut symbols, match_scala_node); + walk_ast_nodes_with_config(&tree.root_node(), source, &mut symbols.ast_nodes, &SCALA_AST_CONFIG); + walk_tree(&tree.root_node(), source, &mut symbols, match_scala_type_map); + symbols + } +} + +// ── Type inference ────────────────────────────────────────────────────────── + +fn match_scala_type_map(node: &Node, source: &[u8], symbols: &mut FileSymbols, _depth: usize) { + match node.kind() { + "val_definition" | "var_definition" => { + if let Some(type_node) = node.child_by_field_name("type") + .or_else(|| find_child(node, "type_identifier")) + { + if let Some(pat) = node.child_by_field_name("pattern") + .or_else(|| find_child(node, "identifier")) + { + symbols.type_map.push(TypeMapEntry { + name: node_text(&pat, source).to_string(), + type_name: node_text(&type_node, source).to_string(), + }); + } + } + } + "parameter" => { + if let Some(type_node) = node.child_by_field_name("type") { + if let Some(name_node) = node.child_by_field_name("name") + .or_else(|| find_child(node, "identifier")) + { + symbols.type_map.push(TypeMapEntry { + name: node_text(&name_node, source).to_string(), + type_name: node_text(&type_node, source).to_string(), + }); + } + } + } + _ => {} + } +} + +// ── Helpers ────────────────────────────────────────────────────────────────── + +fn find_scala_parent_class<'a>(node: &Node<'a>, source: &[u8]) -> Option { + let mut current = node.parent(); + while let Some(parent) = current { + match parent.kind() { + "class_definition" | "object_definition" | "trait_definition" => { + return parent.child_by_field_name("name") + .or_else(|| find_child(&parent, "identifier")) + .map(|n| node_text(&n, source).to_string()); + } + _ => {} + } + current = parent.parent(); + } + None +} + +fn extract_scala_parameters(node: &Node, source: &[u8]) -> Vec { + let mut params = Vec::new(); + if let Some(param_list) = node.child_by_field_name("parameters") + .or_else(|| find_child(node, "parameters")) + { + for i in 0..param_list.child_count() { + if let Some(child) = param_list.child(i) { + if child.kind() == "parameter" { + if let Some(name_node) = child.child_by_field_name("name") + .or_else(|| find_child(&child, "identifier")) + { + params.push(child_def( + node_text(&name_node, source).to_string(), + "parameter", + start_line(&child), + )); + } + } + } + } + } + params +} + +fn extract_scala_class_members(node: &Node, source: &[u8]) -> Vec { + let mut members = Vec::new(); + if let Some(body) = find_child(node, "template_body") { + for i in 0..body.child_count() { + if let Some(child) = body.child(i) { + match child.kind() { + "val_definition" | "var_definition" => { + let name = child.child_by_field_name("pattern") + .or_else(|| find_child(&child, "identifier")) + .map(|n| node_text(&n, source).to_string()); + if let Some(name) = name { + members.push(child_def(name, "property", start_line(&child))); + } + } + _ => {} + } + } + } + } + members +} + +fn extract_scala_extends(node: &Node, source: &[u8], class_name: &str, symbols: &mut FileSymbols) { + if let Some(extends) = find_child(node, "extends_clause") { + // The first type_identifier in the extends clause is the superclass + let mut found_extends = false; + for i in 0..extends.child_count() { + if let Some(child) = extends.child(i) { + match child.kind() { + "type_identifier" | "generic_type" => { + let type_name = if child.kind() == "generic_type" { + child.child(0).map(|n| node_text(&n, source).to_string()) + } else { + Some(node_text(&child, source).to_string()) + }; + if let Some(type_name) = type_name { + if !found_extends { + symbols.classes.push(ClassRelation { + name: class_name.to_string(), + extends: Some(type_name), + implements: None, + line: start_line(node), + }); + found_extends = true; + } else { + symbols.classes.push(ClassRelation { + name: class_name.to_string(), + extends: None, + implements: Some(type_name), + line: start_line(node), + }); + } + } + } + _ => {} + } + } + } + } +} + +/// Extract import path by concatenating alternating identifier and "." children. +fn extract_scala_import_path(node: &Node, source: &[u8]) -> String { + let mut path = String::new(); + for i in 0..node.child_count() { + if let Some(child) = node.child(i) { + match child.kind() { + "identifier" | "type_identifier" => { + if !path.is_empty() && !path.ends_with('.') { + path.push('.'); + } + path.push_str(node_text(&child, source)); + } + "." => { + if !path.is_empty() { + path.push('.'); + } + } + "import_selectors" => { + // e.g. import scala.collection.mutable.{Map, Set} + // Just append the selectors text + if !path.is_empty() && !path.ends_with('.') { + path.push('.'); + } + path.push_str(node_text(&child, source)); + } + "wildcard" => { + if !path.is_empty() && !path.ends_with('.') { + path.push('.'); + } + path.push('_'); + } + // Skip keywords like "import" + _ => {} + } + } + } + path +} + +fn match_scala_node(node: &Node, source: &[u8], symbols: &mut FileSymbols, _depth: usize) { + match node.kind() { + "class_definition" => { + let name_node = node.child_by_field_name("name") + .or_else(|| find_child(node, "identifier")); + if let Some(name_node) = name_node { + let class_name = node_text(&name_node, source).to_string(); + let children = extract_scala_class_members(node, source); + symbols.definitions.push(Definition { + name: class_name.clone(), + kind: "class".to_string(), + line: start_line(node), + end_line: Some(end_line(node)), + decorators: None, + complexity: None, + cfg: None, + children: opt_children(children), + }); + extract_scala_extends(node, source, &class_name, symbols); + } + } + + "trait_definition" => { + let name_node = node.child_by_field_name("name") + .or_else(|| find_child(node, "identifier")); + if let Some(name_node) = name_node { + let trait_name = node_text(&name_node, source).to_string(); + symbols.definitions.push(Definition { + name: trait_name.clone(), + kind: "interface".to_string(), + line: start_line(node), + end_line: Some(end_line(node)), + decorators: None, + complexity: None, + cfg: None, + children: None, + }); + extract_scala_extends(node, source, &trait_name, symbols); + } + } + + "object_definition" => { + let name_node = node.child_by_field_name("name") + .or_else(|| find_child(node, "identifier")); + if let Some(name_node) = name_node { + let obj_name = node_text(&name_node, source).to_string(); + let children = extract_scala_class_members(node, source); + symbols.definitions.push(Definition { + name: obj_name.clone(), + kind: "class".to_string(), + line: start_line(node), + end_line: Some(end_line(node)), + decorators: None, + complexity: None, + cfg: None, + children: opt_children(children), + }); + extract_scala_extends(node, source, &obj_name, symbols); + } + } + + "function_definition" => { + let name_node = node.child_by_field_name("name") + .or_else(|| find_child(node, "identifier")); + if let Some(name_node) = name_node { + let parent_class = find_scala_parent_class(node, source); + let name = node_text(&name_node, source); + let full_name = match &parent_class { + Some(cls) => format!("{}.{}", cls, name), + None => name.to_string(), + }; + let kind = if parent_class.is_some() { "method" } else { "function" }; + let children = extract_scala_parameters(node, source); + symbols.definitions.push(Definition { + name: full_name, + kind: kind.to_string(), + line: start_line(node), + end_line: Some(end_line(node)), + decorators: None, + complexity: compute_all_metrics(node, source, "scala"), + cfg: build_function_cfg(node, "scala", source), + children: opt_children(children), + }); + } + } + + "import_declaration" => { + let path = extract_scala_import_path(node, source); + if !path.is_empty() { + let last = path.split('.').last().unwrap_or("").to_string(); + let mut imp = Import::new(path, vec![last], start_line(node)); + imp.scala_import = Some(true); + symbols.imports.push(imp); + } + } + + "call_expression" => { + if let Some(fn_node) = node.child_by_field_name("function") + .or_else(|| node.child(0)) + { + match fn_node.kind() { + "identifier" => { + symbols.calls.push(Call { + name: node_text(&fn_node, source).to_string(), + line: start_line(node), + dynamic: None, + receiver: None, + }); + } + "field_expression" => { + let name = fn_node.child_by_field_name("field") + .or_else(|| fn_node.child_by_field_name("member")) + .map(|n| node_text(&n, source).to_string()) + .unwrap_or_else(|| node_text(&fn_node, source).to_string()); + let receiver = fn_node.child_by_field_name("value") + .or_else(|| fn_node.child(0)) + .map(|n| node_text(&n, source).to_string()); + symbols.calls.push(Call { + name, + line: start_line(node), + dynamic: None, + receiver, + }); + } + _ => { + symbols.calls.push(Call { + name: node_text(&fn_node, source).to_string(), + line: start_line(node), + dynamic: None, + receiver: None, + }); + } + } + } + } + + _ => {} + } +} + +#[cfg(test)] +mod tests { + use super::*; + use tree_sitter::Parser; + + fn parse_scala(code: &str) -> FileSymbols { + let mut parser = Parser::new(); + parser + .set_language(&tree_sitter_scala::LANGUAGE.into()) + .unwrap(); + let tree = parser.parse(code.as_bytes(), None).unwrap(); + ScalaExtractor.extract(&tree, code.as_bytes(), "Test.scala") + } + + #[test] + fn extracts_class() { + let s = parse_scala("class Foo { val x: Int = 1 }"); + let foo = s.definitions.iter().find(|d| d.name == "Foo").unwrap(); + assert_eq!(foo.kind, "class"); + } + + #[test] + fn extracts_trait() { + let s = parse_scala("trait Drawable { def draw(): Unit }"); + let t = s.definitions.iter().find(|d| d.name == "Drawable").unwrap(); + assert_eq!(t.kind, "interface"); + } + + #[test] + fn extracts_object() { + let s = parse_scala("object Singleton { val x = 1 }"); + let obj = s.definitions.iter().find(|d| d.name == "Singleton").unwrap(); + assert_eq!(obj.kind, "class"); + } + + #[test] + fn extracts_function() { + let s = parse_scala("def greet(name: String): String = name"); + let greet = s.definitions.iter().find(|d| d.name == "greet").unwrap(); + assert_eq!(greet.kind, "function"); + } + + #[test] + fn extracts_import() { + let s = parse_scala("import scala.collection.mutable.Map"); + assert_eq!(s.imports.len(), 1); + assert!(s.imports[0].scala_import.unwrap()); + } +} diff --git a/crates/codegraph-core/src/extractors/swift.rs b/crates/codegraph-core/src/extractors/swift.rs new file mode 100644 index 00000000..347bf331 --- /dev/null +++ b/crates/codegraph-core/src/extractors/swift.rs @@ -0,0 +1,377 @@ +use tree_sitter::{Node, Tree}; +use crate::cfg::build_function_cfg; +use crate::complexity::compute_all_metrics; +use crate::types::*; +use super::helpers::*; +use super::SymbolExtractor; + +pub struct SwiftExtractor; + +impl SymbolExtractor for SwiftExtractor { + fn extract(&self, tree: &Tree, source: &[u8], file_path: &str) -> FileSymbols { + let mut symbols = FileSymbols::new(file_path.to_string()); + walk_tree(&tree.root_node(), source, &mut symbols, match_swift_node); + walk_ast_nodes_with_config(&tree.root_node(), source, &mut symbols.ast_nodes, &SWIFT_AST_CONFIG); + walk_tree(&tree.root_node(), source, &mut symbols, match_swift_type_map); + symbols + } +} + +// ── Type inference ────────────────────────────────────────────────────────── + +fn match_swift_type_map(node: &Node, source: &[u8], symbols: &mut FileSymbols, _depth: usize) { + if node.kind() == "property_declaration" { + if let Some(type_ann) = node.child_by_field_name("type") + .or_else(|| find_child(node, "type_annotation")) + { + // type_annotation contains the actual type as a child + let type_name = if type_ann.kind() == "type_annotation" { + type_ann.child(type_ann.child_count().saturating_sub(1)) + .map(|n| node_text(&n, source)) + .unwrap_or("") + } else { + node_text(&type_ann, source) + }; + if let Some(pat) = find_child(node, "pattern") { + let name = node_text(&pat, source); + if !name.is_empty() && !type_name.is_empty() { + symbols.type_map.push(TypeMapEntry { + name: name.to_string(), + type_name: type_name.to_string(), + }); + } + } + } + } +} + +// ── Helpers ────────────────────────────────────────────────────────────────── + +/// Determine the kind of a `class_declaration` by checking keyword children. +/// Swift uses `class_declaration` for class, struct, and enum. +fn swift_class_kind(node: &Node, source: &[u8]) -> &'static str { + for i in 0..node.child_count() { + if let Some(child) = node.child(i) { + let text = node_text(&child, source); + match text { + "struct" => return "struct", + "enum" => return "enum", + "class" => return "class", + _ => {} + } + } + } + "class" +} + +fn find_swift_parent_class<'a>(node: &Node<'a>, source: &[u8]) -> Option { + let mut current = node.parent(); + while let Some(parent) = current { + match parent.kind() { + "class_declaration" | "protocol_declaration" => { + return find_child(&parent, "simple_identifier") + .or_else(|| parent.child_by_field_name("name")) + .map(|n| node_text(&n, source).to_string()); + } + _ => {} + } + current = parent.parent(); + } + None +} + +fn extract_swift_parameters(node: &Node, source: &[u8]) -> Vec { + let mut params = Vec::new(); + // Look for parameter clauses + for i in 0..node.child_count() { + if let Some(child) = node.child(i) { + if child.kind() == "function_value_parameters" + || child.kind() == "parameter_clause" + || child.kind() == "lambda_function_type_parameters" + { + for j in 0..child.child_count() { + if let Some(param) = child.child(j) { + if param.kind() == "parameter" { + // Swift params have external and internal names + // The internal name (or only name) is what we want + let name = param.child_by_field_name("internal_name") + .or_else(|| param.child_by_field_name("name")) + .or_else(|| find_child(¶m, "simple_identifier")) + .map(|n| node_text(&n, source).to_string()); + if let Some(name) = name { + params.push(child_def(name, "parameter", start_line(¶m))); + } + } + } + } + } + } + } + params +} + +fn extract_swift_class_properties(node: &Node, source: &[u8]) -> Vec { + let mut props = Vec::new(); + let body = find_child(node, "class_body") + .or_else(|| find_child(node, "enum_class_body")); + if let Some(body) = body { + for i in 0..body.child_count() { + if let Some(child) = body.child(i) { + if child.kind() == "property_declaration" { + if let Some(pat) = find_child(&child, "pattern") { + props.push(child_def( + node_text(&pat, source).to_string(), + "property", + start_line(&child), + )); + } + } + } + } + } + props +} + +fn extract_swift_enum_cases(node: &Node, source: &[u8]) -> Vec { + let mut cases = Vec::new(); + let body = find_child(node, "enum_class_body") + .or_else(|| find_child(node, "class_body")); + if let Some(body) = body { + for i in 0..body.child_count() { + if let Some(child) = body.child(i) { + if child.kind() == "enum_entry" { + if let Some(name_node) = find_child(&child, "simple_identifier") { + cases.push(child_def( + node_text(&name_node, source).to_string(), + "constant", + start_line(&child), + )); + } + } + } + } + } + cases +} + +fn extract_swift_inheritance(node: &Node, source: &[u8], class_name: &str, symbols: &mut FileSymbols) { + let mut first = true; + for i in 0..node.child_count() { + if let Some(child) = node.child(i) { + if child.kind() == "inheritance_specifier" { + // inheritance_specifier contains user_type > type_identifier + if let Some(user_type) = find_child(&child, "user_type") { + if let Some(type_id) = find_child(&user_type, "type_identifier") { + let type_name = node_text(&type_id, source).to_string(); + if first { + // First inheritance specifier is typically extends + symbols.classes.push(ClassRelation { + name: class_name.to_string(), + extends: Some(type_name), + implements: None, + line: start_line(node), + }); + first = false; + } else { + symbols.classes.push(ClassRelation { + name: class_name.to_string(), + extends: None, + implements: Some(type_name), + line: start_line(node), + }); + } + } + } + } + } + } +} + +fn match_swift_node(node: &Node, source: &[u8], symbols: &mut FileSymbols, _depth: usize) { + match node.kind() { + "class_declaration" => { + let name_node = find_child(node, "simple_identifier") + .or_else(|| node.child_by_field_name("name")); + if let Some(name_node) = name_node { + let class_name = node_text(&name_node, source).to_string(); + let kind = swift_class_kind(node, source); + + match kind { + "enum" => { + let children = extract_swift_enum_cases(node, source); + symbols.definitions.push(Definition { + name: class_name.clone(), + kind: "enum".to_string(), + line: start_line(node), + end_line: Some(end_line(node)), + decorators: None, + complexity: None, + cfg: None, + children: opt_children(children), + }); + } + _ => { + let children = extract_swift_class_properties(node, source); + symbols.definitions.push(Definition { + name: class_name.clone(), + kind: kind.to_string(), + line: start_line(node), + end_line: Some(end_line(node)), + decorators: None, + complexity: None, + cfg: None, + children: opt_children(children), + }); + } + } + + extract_swift_inheritance(node, source, &class_name, symbols); + } + } + + "protocol_declaration" => { + let name_node = find_child(node, "simple_identifier") + .or_else(|| node.child_by_field_name("name")); + if let Some(name_node) = name_node { + let proto_name = node_text(&name_node, source).to_string(); + symbols.definitions.push(Definition { + name: proto_name.clone(), + kind: "interface".to_string(), + line: start_line(node), + end_line: Some(end_line(node)), + decorators: None, + complexity: None, + cfg: None, + children: None, + }); + // Protocol can also have inheritance + extract_swift_inheritance(node, source, &proto_name, symbols); + } + } + + "function_declaration" => { + let name_node = find_child(node, "simple_identifier") + .or_else(|| node.child_by_field_name("name")); + if let Some(name_node) = name_node { + let parent_class = find_swift_parent_class(node, source); + let name = node_text(&name_node, source); + let full_name = match &parent_class { + Some(cls) => format!("{}.{}", cls, name), + None => name.to_string(), + }; + let kind = if parent_class.is_some() { "method" } else { "function" }; + let children = extract_swift_parameters(node, source); + symbols.definitions.push(Definition { + name: full_name, + kind: kind.to_string(), + line: start_line(node), + end_line: Some(end_line(node)), + decorators: None, + complexity: compute_all_metrics(node, source, "swift"), + cfg: build_function_cfg(node, "swift", source), + children: opt_children(children), + }); + } + } + + "import_declaration" => { + if let Some(id_node) = find_child(node, "identifier") { + let path = node_text(&id_node, source).to_string(); + let last = path.split('.').last().unwrap_or(&path).to_string(); + let mut imp = Import::new(path, vec![last], start_line(node)); + imp.swift_import = Some(true); + symbols.imports.push(imp); + } + } + + "call_expression" => { + if let Some(fn_node) = node.child(0) { + match fn_node.kind() { + "simple_identifier" => { + symbols.calls.push(Call { + name: node_text(&fn_node, source).to_string(), + line: start_line(node), + dynamic: None, + receiver: None, + }); + } + "navigation_expression" => { + let last = fn_node.child(fn_node.child_count().saturating_sub(1)); + let name = last + .map(|n| node_text(&n, source).to_string()) + .unwrap_or_else(|| node_text(&fn_node, source).to_string()); + let receiver = fn_node.child(0) + .map(|n| node_text(&n, source).to_string()); + symbols.calls.push(Call { + name, + line: start_line(node), + dynamic: None, + receiver, + }); + } + _ => { + symbols.calls.push(Call { + name: node_text(&fn_node, source).to_string(), + line: start_line(node), + dynamic: None, + receiver: None, + }); + } + } + } + } + + _ => {} + } +} + +#[cfg(test)] +mod tests { + use super::*; + use tree_sitter::Parser; + + fn parse_swift(code: &str) -> FileSymbols { + let mut parser = Parser::new(); + parser + .set_language(&tree_sitter_swift::LANGUAGE.into()) + .unwrap(); + let tree = parser.parse(code.as_bytes(), None).unwrap(); + SwiftExtractor.extract(&tree, code.as_bytes(), "Test.swift") + } + + #[test] + fn extracts_class() { + let s = parse_swift("class Foo { var x: Int = 0 }"); + let foo = s.definitions.iter().find(|d| d.name == "Foo").unwrap(); + assert_eq!(foo.kind, "class"); + } + + #[test] + fn extracts_struct() { + let s = parse_swift("struct Point { var x: Int; var y: Int }"); + let point = s.definitions.iter().find(|d| d.name == "Point").unwrap(); + assert_eq!(point.kind, "struct"); + } + + #[test] + fn extracts_protocol() { + let s = parse_swift("protocol Drawable { func draw() }"); + let proto = s.definitions.iter().find(|d| d.name == "Drawable").unwrap(); + assert_eq!(proto.kind, "interface"); + } + + #[test] + fn extracts_function() { + let s = parse_swift("func greet(name: String) -> String { return name }"); + let greet = s.definitions.iter().find(|d| d.name == "greet").unwrap(); + assert_eq!(greet.kind, "function"); + } + + #[test] + fn extracts_import() { + let s = parse_swift("import Foundation"); + assert_eq!(s.imports.len(), 1); + assert_eq!(s.imports[0].path, "Foundation"); + assert!(s.imports[0].swift_import.unwrap()); + } +} diff --git a/crates/codegraph-core/src/parser_registry.rs b/crates/codegraph-core/src/parser_registry.rs index 0dde0bd6..ea2a64dc 100644 --- a/crates/codegraph-core/src/parser_registry.rs +++ b/crates/codegraph-core/src/parser_registry.rs @@ -14,6 +14,12 @@ pub enum LanguageKind { Ruby, Php, Hcl, + C, + Cpp, + Kotlin, + Swift, + Scala, + Bash, } impl LanguageKind { @@ -32,6 +38,12 @@ impl LanguageKind { Self::Ruby => "ruby", Self::Php => "php", Self::Hcl => "hcl", + Self::C => "c", + Self::Cpp => "cpp", + Self::Kotlin => "kotlin", + Self::Swift => "swift", + Self::Scala => "scala", + Self::Bash => "bash", } } @@ -58,6 +70,12 @@ impl LanguageKind { "cs" => Some(Self::CSharp), "rb" | "rake" | "gemspec" => Some(Self::Ruby), "php" | "phtml" => Some(Self::Php), + "c" | "h" => Some(Self::C), + "cpp" | "cc" | "cxx" | "hpp" => Some(Self::Cpp), + "kt" | "kts" => Some(Self::Kotlin), + "swift" => Some(Self::Swift), + "scala" => Some(Self::Scala), + "sh" | "bash" => Some(Self::Bash), _ => None, } } @@ -76,6 +94,12 @@ impl LanguageKind { Self::Ruby => tree_sitter_ruby::LANGUAGE.into(), Self::Php => tree_sitter_php::LANGUAGE_PHP.into(), Self::Hcl => tree_sitter_hcl::LANGUAGE.into(), + Self::C => tree_sitter_c::LANGUAGE.into(), + Self::Cpp => tree_sitter_cpp::LANGUAGE.into(), + Self::Kotlin => tree_sitter_kotlin_sg::LANGUAGE.into(), + Self::Swift => tree_sitter_swift::LANGUAGE.into(), + Self::Scala => tree_sitter_scala::LANGUAGE.into(), + Self::Bash => tree_sitter_bash::LANGUAGE.into(), } } } diff --git a/crates/codegraph-core/src/types.rs b/crates/codegraph-core/src/types.rs index 77ea559d..de58dc66 100644 --- a/crates/codegraph-core/src/types.rs +++ b/crates/codegraph-core/src/types.rs @@ -138,6 +138,16 @@ pub struct Import { pub php_use: Option, #[napi(js_name = "dynamicImport")] pub dynamic_import: Option, + #[napi(js_name = "cInclude")] + pub c_include: Option, + #[napi(js_name = "kotlinImport")] + pub kotlin_import: Option, + #[napi(js_name = "swiftImport")] + pub swift_import: Option, + #[napi(js_name = "scalaImport")] + pub scala_import: Option, + #[napi(js_name = "bashSource")] + pub bash_source: Option, } impl Import { @@ -157,6 +167,11 @@ impl Import { ruby_require: None, php_use: None, dynamic_import: None, + c_include: None, + kotlin_import: None, + swift_import: None, + scala_import: None, + bash_source: None, } } } diff --git a/package-lock.json b/package-lock.json index 51541933..14670082 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,15 +26,21 @@ "@vitest/coverage-v8": "^4.0.18", "commit-and-tag-version": "^12.5", "husky": "^9.1", + "tree-sitter-bash": "^0.25.1", + "tree-sitter-c": "^0.24.1", "tree-sitter-c-sharp": "^0.23.1", "tree-sitter-cli": "^0.26.5", + "tree-sitter-cpp": "^0.23.4", "tree-sitter-go": "^0.25.0", "tree-sitter-java": "^0.23.5", "tree-sitter-javascript": "^0.25.0", + "tree-sitter-kotlin": "^0.3.8", "tree-sitter-php": "^0.24.2", "tree-sitter-python": "^0.25.0", "tree-sitter-ruby": "^0.23.1", "tree-sitter-rust": "^0.24.0", + "tree-sitter-scala": "^0.24.0", + "tree-sitter-swift": "^0.7.1", "tree-sitter-typescript": "^0.23.2", "typescript": "^6.0.2", "vitest": "^4.0.18" @@ -1276,9 +1282,6 @@ "cpu": [ "arm64" ], - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -1292,9 +1295,6 @@ "cpu": [ "x64" ], - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -1308,9 +1308,6 @@ "cpu": [ "x64" ], - "libc": [ - "musl" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -4520,8 +4517,8 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "license": "ISC", - "optional": true + "devOptional": true, + "license": "ISC" }, "node_modules/istanbul-lib-coverage": { "version": "3.2.2", @@ -6989,6 +6986,46 @@ "node": ">=0.6" } }, + "node_modules/tree-sitter-bash": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/tree-sitter-bash/-/tree-sitter-bash-0.25.1.tgz", + "integrity": "sha512-7hMytuYIMoXOq24yRulgIxthE9YmggZIOHCyPTTuJcu6EU54tYD+4G39cUb28kxC6jMf/AbPfWGLQtgPTdh3xw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "node-addon-api": "^8.2.1", + "node-gyp-build": "^4.8.2" + }, + "peerDependencies": { + "tree-sitter": "^0.25.0" + }, + "peerDependenciesMeta": { + "tree-sitter": { + "optional": true + } + } + }, + "node_modules/tree-sitter-c": { + "version": "0.24.1", + "resolved": "https://registry.npmjs.org/tree-sitter-c/-/tree-sitter-c-0.24.1.tgz", + "integrity": "sha512-lkYwWN3SRecpvaeqmFKkuPNR3ZbtnvHU+4XAEEkJdrp3JfSp2pBrhXOtvfsENUneye76g889Y0ddF2DM0gEDpA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "node-addon-api": "^8.3.1", + "node-gyp-build": "^4.8.4" + }, + "peerDependencies": { + "tree-sitter": "^0.22.4" + }, + "peerDependenciesMeta": { + "tree-sitter": { + "optional": true + } + } + }, "node_modules/tree-sitter-c-sharp": { "version": "0.23.1", "resolved": "https://registry.npmjs.org/tree-sitter-c-sharp/-/tree-sitter-c-sharp-0.23.1.tgz", @@ -7023,6 +7060,47 @@ "node": ">=12.0.0" } }, + "node_modules/tree-sitter-cpp": { + "version": "0.23.4", + "resolved": "https://registry.npmjs.org/tree-sitter-cpp/-/tree-sitter-cpp-0.23.4.tgz", + "integrity": "sha512-qR5qUDyhZ5jJ6V8/umiBxokRbe89bCGmcq/dk94wI4kN86qfdV8k0GHIUEKaqWgcu42wKal5E97LKpLeVW8sKw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "node-addon-api": "^8.2.1", + "node-gyp-build": "^4.8.2", + "tree-sitter-c": "^0.23.1" + }, + "peerDependencies": { + "tree-sitter": "^0.21.1" + }, + "peerDependenciesMeta": { + "tree-sitter": { + "optional": true + } + } + }, + "node_modules/tree-sitter-cpp/node_modules/tree-sitter-c": { + "version": "0.23.6", + "resolved": "https://registry.npmjs.org/tree-sitter-c/-/tree-sitter-c-0.23.6.tgz", + "integrity": "sha512-0dxXKznVyUA0s6PjNolJNs2yF87O5aL538A/eR6njA5oqX3C3vH4vnx3QdOKwuUdpKEcFdHuiDpRKLLCA/tjvQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "node-addon-api": "^8.3.0", + "node-gyp-build": "^4.8.4" + }, + "peerDependencies": { + "tree-sitter": "^0.22.1" + }, + "peerDependenciesMeta": { + "tree-sitter": { + "optional": true + } + } + }, "node_modules/tree-sitter-go": { "version": "0.25.0", "resolved": "https://registry.npmjs.org/tree-sitter-go/-/tree-sitter-go-0.25.0.tgz", @@ -7083,6 +7161,33 @@ } } }, + "node_modules/tree-sitter-kotlin": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/tree-sitter-kotlin/-/tree-sitter-kotlin-0.3.8.tgz", + "integrity": "sha512-A4obq6bjzmYrA+F0JLLoheFPcofFkctNaZSpnDd+GPn1SfVZLY4/GG4C0cYVBTOShuPBGGAOPLM1JWLZQV4m1g==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "node-addon-api": "^7.1.0", + "node-gyp-build": "^4.8.0" + }, + "peerDependencies": { + "tree-sitter": "^0.21.0" + }, + "peerDependenciesMeta": { + "tree_sitter": { + "optional": true + } + } + }, + "node_modules/tree-sitter-kotlin/node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "dev": true, + "license": "MIT" + }, "node_modules/tree-sitter-php": { "version": "0.24.2", "resolved": "https://registry.npmjs.org/tree-sitter-php/-/tree-sitter-php-0.24.2.tgz", @@ -7163,6 +7268,62 @@ } } }, + "node_modules/tree-sitter-scala": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/tree-sitter-scala/-/tree-sitter-scala-0.24.0.tgz", + "integrity": "sha512-vkMuAUrBZ1zZz2XcGDQk18Kz73JkpgaeXzbNVobPke0G35sd9jH32aUxG6OLRKM7et0TbsfqkWf4DeJoGk4K1g==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "node-addon-api": "^8.2.2", + "node-gyp-build": "^4.8.2" + }, + "peerDependencies": { + "tree-sitter": "^0.21.1" + }, + "peerDependenciesMeta": { + "tree-sitter": { + "optional": true + } + } + }, + "node_modules/tree-sitter-swift": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/tree-sitter-swift/-/tree-sitter-swift-0.7.1.tgz", + "integrity": "sha512-pneKVTuGamaBsqqqfB9BvNQjktzh/0IVPR54jLB5Fq/JTDQwYHd0Wo6pVyZ5jAYpbztzq+rJ/rpL9ruxTmSoKw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "node-addon-api": "^8.0.0", + "node-gyp-build": "^4.8.0", + "tree-sitter-cli": "^0.23", + "which": "2.0.2" + }, + "peerDependencies": { + "tree-sitter": "^0.22.1" + }, + "peerDependenciesMeta": { + "tree_sitter": { + "optional": true + } + } + }, + "node_modules/tree-sitter-swift/node_modules/tree-sitter-cli": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/tree-sitter-cli/-/tree-sitter-cli-0.23.2.tgz", + "integrity": "sha512-kPPXprOqREX+C/FgUp2Qpt9jd0vSwn+hOgjzVv/7hapdoWpa+VeWId53rf4oNNd29ikheF12BYtGD/W90feMbA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "tree-sitter": "cli.js" + }, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/tree-sitter-typescript": { "version": "0.23.2", "resolved": "https://registry.npmjs.org/tree-sitter-typescript/-/tree-sitter-typescript-0.23.2.tgz", @@ -7511,8 +7672,8 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "devOptional": true, "license": "ISC", - "optional": true, "dependencies": { "isexe": "^2.0.0" }, diff --git a/package.json b/package.json index a1e030db..fe177e6e 100644 --- a/package.json +++ b/package.json @@ -148,15 +148,21 @@ "@vitest/coverage-v8": "^4.0.18", "commit-and-tag-version": "^12.5", "husky": "^9.1", + "tree-sitter-bash": "^0.25.1", + "tree-sitter-c": "^0.24.1", "tree-sitter-c-sharp": "^0.23.1", + "tree-sitter-cpp": "^0.23.4", "tree-sitter-cli": "^0.26.5", "tree-sitter-go": "^0.25.0", "tree-sitter-java": "^0.23.5", "tree-sitter-javascript": "^0.25.0", + "tree-sitter-kotlin": "^0.3.8", "tree-sitter-php": "^0.24.2", "tree-sitter-python": "^0.25.0", "tree-sitter-ruby": "^0.23.1", "tree-sitter-rust": "^0.24.0", + "tree-sitter-scala": "^0.24.0", + "tree-sitter-swift": "^0.7.1", "tree-sitter-typescript": "^0.23.2", "typescript": "^6.0.2", "vitest": "^4.0.18" diff --git a/scripts/build-wasm.ts b/scripts/build-wasm.ts index c2fbd997..692a6e56 100644 --- a/scripts/build-wasm.ts +++ b/scripts/build-wasm.ts @@ -34,6 +34,12 @@ const grammars = [ { name: 'tree-sitter-c-sharp', pkg: 'tree-sitter-c-sharp', sub: null }, { name: 'tree-sitter-ruby', pkg: 'tree-sitter-ruby', sub: null }, { name: 'tree-sitter-php', pkg: 'tree-sitter-php', sub: 'php' }, + { name: 'tree-sitter-c', pkg: 'tree-sitter-c', sub: null }, + { name: 'tree-sitter-cpp', pkg: 'tree-sitter-cpp', sub: null }, + { name: 'tree-sitter-kotlin', pkg: 'tree-sitter-kotlin', sub: null }, + { name: 'tree-sitter-swift', pkg: 'tree-sitter-swift', sub: null }, + { name: 'tree-sitter-scala', pkg: 'tree-sitter-scala', sub: null }, + { name: 'tree-sitter-bash', pkg: 'tree-sitter-bash', sub: null }, ]; let failed = 0; diff --git a/src/domain/parser.ts b/src/domain/parser.ts index bc38e312..5ec638b3 100644 --- a/src/domain/parser.ts +++ b/src/domain/parser.ts @@ -16,26 +16,38 @@ import type { // Re-export all extractors for backward compatibility export { + extractBashSymbols, + extractCppSymbols, extractCSharpSymbols, + extractCSymbols, extractGoSymbols, extractHCLSymbols, extractJavaSymbols, + extractKotlinSymbols, extractPHPSymbols, extractPythonSymbols, extractRubySymbols, extractRustSymbols, + extractScalaSymbols, + extractSwiftSymbols, extractSymbols, } from '../extractors/index.js'; import { + extractBashSymbols, + extractCppSymbols, extractCSharpSymbols, + extractCSymbols, extractGoSymbols, extractHCLSymbols, extractJavaSymbols, + extractKotlinSymbols, extractPHPSymbols, extractPythonSymbols, extractRubySymbols, extractRustSymbols, + extractScalaSymbols, + extractSwiftSymbols, extractSymbols, } from '../extractors/index.js'; @@ -294,6 +306,11 @@ function patchImports(imports: any[]): void { if (i.csharpUsing === undefined) i.csharpUsing = i.csharp_using; if (i.rubyRequire === undefined) i.rubyRequire = i.ruby_require; if (i.phpUse === undefined) i.phpUse = i.php_use; + if (i.cInclude === undefined) i.cInclude = i.c_include; + if (i.kotlinImport === undefined) i.kotlinImport = i.kotlin_import; + if (i.swiftImport === undefined) i.swiftImport = i.swift_import; + if (i.scalaImport === undefined) i.scalaImport = i.scala_import; + if (i.bashSource === undefined) i.bashSource = i.bash_source; if (i.dynamicImport === undefined) i.dynamicImport = i.dynamic_import; } } @@ -421,6 +438,48 @@ export const LANGUAGE_REGISTRY: LanguageRegistryEntry[] = [ extractor: extractPHPSymbols, required: false, }, + { + id: 'c', + extensions: ['.c', '.h'], + grammarFile: 'tree-sitter-c.wasm', + extractor: extractCSymbols, + required: false, + }, + { + id: 'cpp', + extensions: ['.cpp', '.cc', '.cxx', '.hpp'], + grammarFile: 'tree-sitter-cpp.wasm', + extractor: extractCppSymbols, + required: false, + }, + { + id: 'kotlin', + extensions: ['.kt', '.kts'], + grammarFile: 'tree-sitter-kotlin.wasm', + extractor: extractKotlinSymbols, + required: false, + }, + { + id: 'swift', + extensions: ['.swift'], + grammarFile: 'tree-sitter-swift.wasm', + extractor: extractSwiftSymbols, + required: false, + }, + { + id: 'scala', + extensions: ['.scala'], + grammarFile: 'tree-sitter-scala.wasm', + extractor: extractScalaSymbols, + required: false, + }, + { + id: 'bash', + extensions: ['.sh', '.bash'], + grammarFile: 'tree-sitter-bash.wasm', + extractor: extractBashSymbols, + required: false, + }, ]; const _extToLang: Map = new Map(); diff --git a/src/extractors/bash.ts b/src/extractors/bash.ts new file mode 100644 index 00000000..fa4e849b --- /dev/null +++ b/src/extractors/bash.ts @@ -0,0 +1,97 @@ +import type { Call, ExtractorOutput, TreeSitterNode, TreeSitterTree } from '../types.js'; +import { nodeEndLine } from './helpers.js'; + +/** + * Extract symbols from Bash/Shell files. + */ +export function extractBashSymbols(tree: TreeSitterTree, _filePath: string): ExtractorOutput { + const ctx: ExtractorOutput = { + definitions: [], + calls: [], + imports: [], + classes: [], + exports: [], + typeMap: new Map(), + }; + + walkBashNode(tree.rootNode, ctx); + return ctx; +} + +function walkBashNode(node: TreeSitterNode, ctx: ExtractorOutput): void { + switch (node.type) { + case 'function_definition': + handleBashFunctionDef(node, ctx); + break; + case 'command': + handleBashCommand(node, ctx); + break; + } + + for (let i = 0; i < node.childCount; i++) { + const child = node.child(i); + if (child) walkBashNode(child, ctx); + } +} + +// ── Walk-path per-node-type handlers ──────────────────────────────────────── + +function handleBashFunctionDef(node: TreeSitterNode, ctx: ExtractorOutput): void { + const nameNode = node.childForFieldName('name'); + if (!nameNode) return; + ctx.definitions.push({ + name: nameNode.text, + kind: 'function', + line: node.startPosition.row + 1, + endLine: nodeEndLine(node), + }); +} + +function handleBashCommand(node: TreeSitterNode, ctx: ExtractorOutput): void { + // First child is command_name + let commandNameNode: TreeSitterNode | null = null; + for (let i = 0; i < node.childCount; i++) { + const child = node.child(i); + if (child && child.type === 'command_name') { + commandNameNode = child; + break; + } + } + if (!commandNameNode) return; + + const cmdText = commandNameNode.text; + + // "source" or "." commands are imports + if (cmdText === 'source' || cmdText === '.') { + // Second argument is the source path + let argNode: TreeSitterNode | null = null; + let foundCmd = false; + for (let i = 0; i < node.childCount; i++) { + const child = node.child(i); + if (!child) continue; + if (child.type === 'command_name') { + foundCmd = true; + continue; + } + if (foundCmd && child.type !== 'command_name') { + argNode = child; + break; + } + } + if (argNode) { + const source = argNode.text; + const lastName = source.split('/').pop() ?? source; + ctx.imports.push({ + source, + names: [lastName], + line: node.startPosition.row + 1, + bashSource: true, + }); + } + return; + } + + // Regular command call + const call: Call = { name: cmdText, line: node.startPosition.row + 1 }; + ctx.calls.push(call); +} diff --git a/src/extractors/c.ts b/src/extractors/c.ts new file mode 100644 index 00000000..8ece4df4 --- /dev/null +++ b/src/extractors/c.ts @@ -0,0 +1,212 @@ +import type { + Call, + ExtractorOutput, + SubDeclaration, + TreeSitterNode, + TreeSitterTree, +} from '../types.js'; +import { findChild, nodeEndLine } from './helpers.js'; + +/** + * Extract symbols from C files. + */ +export function extractCSymbols(tree: TreeSitterTree, _filePath: string): ExtractorOutput { + const ctx: ExtractorOutput = { + definitions: [], + calls: [], + imports: [], + classes: [], + exports: [], + typeMap: new Map(), + }; + + walkCNode(tree.rootNode, ctx); + return ctx; +} + +function walkCNode(node: TreeSitterNode, ctx: ExtractorOutput): void { + switch (node.type) { + case 'function_definition': + handleCFunctionDef(node, ctx); + break; + case 'struct_specifier': + handleCStructSpecifier(node, ctx); + break; + case 'enum_specifier': + handleCEnumSpecifier(node, ctx); + break; + case 'type_definition': + handleCTypedef(node, ctx); + break; + case 'preproc_include': + handleCInclude(node, ctx); + break; + case 'call_expression': + handleCCallExpression(node, ctx); + break; + } + + for (let i = 0; i < node.childCount; i++) { + const child = node.child(i); + if (child) walkCNode(child, ctx); + } +} + +// ── Walk-path per-node-type handlers ──────────────────────────────────────── + +function handleCFunctionDef(node: TreeSitterNode, ctx: ExtractorOutput): void { + // declarator > function_declarator > declarator(identifier) + const declarator = node.childForFieldName('declarator'); + if (!declarator) return; + const funcDeclarator = + declarator.type === 'function_declarator' + ? declarator + : findChild(declarator, 'function_declarator'); + if (!funcDeclarator) return; + const nameNode = funcDeclarator.childForFieldName('declarator'); + if (!nameNode) return; + const name = nameNode.type === 'identifier' ? nameNode.text : nameNode.text; + + const params = extractCParameters(funcDeclarator.childForFieldName('parameters')); + ctx.definitions.push({ + name, + kind: 'function', + line: node.startPosition.row + 1, + endLine: nodeEndLine(node), + children: params.length > 0 ? params : undefined, + }); +} + +function handleCStructSpecifier(node: TreeSitterNode, ctx: ExtractorOutput): void { + const nameNode = node.childForFieldName('name'); + if (!nameNode) return; + const children = extractStructFields(node); + ctx.definitions.push({ + name: nameNode.text, + kind: 'struct', + line: node.startPosition.row + 1, + endLine: nodeEndLine(node), + children: children.length > 0 ? children : undefined, + }); +} + +function handleCEnumSpecifier(node: TreeSitterNode, ctx: ExtractorOutput): void { + const nameNode = node.childForFieldName('name'); + if (!nameNode) return; + const children = extractEnumEntries(node); + ctx.definitions.push({ + name: nameNode.text, + kind: 'enum', + line: node.startPosition.row + 1, + endLine: nodeEndLine(node), + children: children.length > 0 ? children : undefined, + }); +} + +function handleCTypedef(node: TreeSitterNode, ctx: ExtractorOutput): void { + // The typedef name is the last type_identifier, identifier, or primitive_type child + let name: string | undefined; + for (let i = node.childCount - 1; i >= 0; i--) { + const child = node.child(i); + if ( + child && + (child.type === 'type_identifier' || + child.type === 'identifier' || + child.type === 'primitive_type') + ) { + name = child.text; + break; + } + } + if (!name) return; + ctx.definitions.push({ + name, + kind: 'type', + line: node.startPosition.row + 1, + endLine: nodeEndLine(node), + }); +} + +function handleCInclude(node: TreeSitterNode, ctx: ExtractorOutput): void { + const pathNode = node.childForFieldName('path'); + if (!pathNode) return; + // Strip quotes or angle brackets + const raw = pathNode.text; + const source = raw.replace(/^["<]|[">]$/g, ''); + const lastName = source.split('/').pop() ?? source; + ctx.imports.push({ + source, + names: [lastName], + line: node.startPosition.row + 1, + cInclude: true, + }); +} + +function handleCCallExpression(node: TreeSitterNode, ctx: ExtractorOutput): void { + const funcNode = node.childForFieldName('function'); + if (!funcNode) return; + const call: Call = { name: '', line: node.startPosition.row + 1 }; + if (funcNode.type === 'field_expression') { + const field = funcNode.childForFieldName('field'); + const argument = funcNode.childForFieldName('argument'); + if (field) call.name = field.text; + if (argument) call.receiver = argument.text; + } else { + call.name = funcNode.text; + } + if (call.name) ctx.calls.push(call); +} + +// ── Child extraction helpers ──────────────────────────────────────────────── + +function extractCParameters(paramListNode: TreeSitterNode | null): SubDeclaration[] { + const params: SubDeclaration[] = []; + if (!paramListNode) return params; + for (let i = 0; i < paramListNode.childCount; i++) { + const param = paramListNode.child(i); + if (!param || param.type !== 'parameter_declaration') continue; + const nameNode = param.childForFieldName('declarator'); + if (nameNode) { + const name = + nameNode.type === 'identifier' + ? nameNode.text + : (findChild(nameNode, 'identifier')?.text ?? nameNode.text); + params.push({ name, kind: 'parameter', line: param.startPosition.row + 1 }); + } + } + return params; +} + +function extractStructFields(structNode: TreeSitterNode): SubDeclaration[] { + const fields: SubDeclaration[] = []; + const body = findChild(structNode, 'field_declaration_list'); + if (!body) return fields; + for (let i = 0; i < body.childCount; i++) { + const member = body.child(i); + if (!member || member.type !== 'field_declaration') continue; + const nameNode = member.childForFieldName('declarator'); + if (nameNode) { + const name = + nameNode.type === 'identifier' + ? nameNode.text + : (findChild(nameNode, 'identifier')?.text ?? nameNode.text); + fields.push({ name, kind: 'property', line: member.startPosition.row + 1 }); + } + } + return fields; +} + +function extractEnumEntries(enumNode: TreeSitterNode): SubDeclaration[] { + const entries: SubDeclaration[] = []; + const body = findChild(enumNode, 'enumerator_list'); + if (!body) return entries; + for (let i = 0; i < body.childCount; i++) { + const member = body.child(i); + if (!member || member.type !== 'enumerator') continue; + const nameNode = member.childForFieldName('name'); + if (nameNode) { + entries.push({ name: nameNode.text, kind: 'constant', line: member.startPosition.row + 1 }); + } + } + return entries; +} diff --git a/src/extractors/cpp.ts b/src/extractors/cpp.ts new file mode 100644 index 00000000..77a33669 --- /dev/null +++ b/src/extractors/cpp.ts @@ -0,0 +1,298 @@ +import type { + Call, + ExtractorOutput, + SubDeclaration, + TreeSitterNode, + TreeSitterTree, +} from '../types.js'; +import { extractModifierVisibility, findChild, nodeEndLine } from './helpers.js'; + +/** + * Extract symbols from C++ files. + */ +export function extractCppSymbols(tree: TreeSitterTree, _filePath: string): ExtractorOutput { + const ctx: ExtractorOutput = { + definitions: [], + calls: [], + imports: [], + classes: [], + exports: [], + typeMap: new Map(), + }; + + walkCppNode(tree.rootNode, ctx); + return ctx; +} + +function walkCppNode(node: TreeSitterNode, ctx: ExtractorOutput): void { + switch (node.type) { + case 'function_definition': + handleCppFunctionDef(node, ctx); + break; + case 'class_specifier': + handleCppClassSpecifier(node, ctx); + break; + case 'struct_specifier': + handleCppStructSpecifier(node, ctx); + break; + case 'enum_specifier': + handleCppEnumSpecifier(node, ctx); + break; + case 'namespace_definition': + handleCppNamespaceDef(node, ctx); + break; + case 'type_definition': + handleCppTypedef(node, ctx); + break; + case 'preproc_include': + handleCppInclude(node, ctx); + break; + case 'call_expression': + handleCppCallExpression(node, ctx); + break; + } + + for (let i = 0; i < node.childCount; i++) { + const child = node.child(i); + if (child) walkCppNode(child, ctx); + } +} + +// ── Walk-path per-node-type handlers ──────────────────────────────────────── + +function handleCppFunctionDef(node: TreeSitterNode, ctx: ExtractorOutput): void { + const declarator = node.childForFieldName('declarator'); + if (!declarator) return; + const funcDeclarator = + declarator.type === 'function_declarator' + ? declarator + : findChild(declarator, 'function_declarator'); + if (!funcDeclarator) return; + const nameNode = funcDeclarator.childForFieldName('declarator'); + if (!nameNode) return; + const name = nameNode.text; + + // If this function is inside a class/struct field_declaration_list, emit as method + const parentClass = findCppParentClass(node); + const fullName = parentClass ? `${parentClass}.${name}` : name; + const kind = parentClass ? 'method' : 'function'; + + const params = extractCppParameters(funcDeclarator.childForFieldName('parameters')); + ctx.definitions.push({ + name: fullName, + kind, + line: node.startPosition.row + 1, + endLine: nodeEndLine(node), + children: params.length > 0 ? params : undefined, + visibility: parentClass ? extractModifierVisibility(node) : undefined, + }); +} + +function handleCppClassSpecifier(node: TreeSitterNode, ctx: ExtractorOutput): void { + const nameNode = node.childForFieldName('name'); + if (!nameNode) return; + const children = extractCppClassFields(node); + ctx.definitions.push({ + name: nameNode.text, + kind: 'class', + line: node.startPosition.row + 1, + endLine: nodeEndLine(node), + children: children.length > 0 ? children : undefined, + }); + + // Inheritance via base_class_clause + const baseClause = findChild(node, 'base_class_clause'); + if (baseClause) { + for (let i = 0; i < baseClause.childCount; i++) { + const child = baseClause.child(i); + if (child && (child.type === 'type_identifier' || child.type === 'qualified_identifier')) { + ctx.classes.push({ + name: nameNode.text, + extends: child.text, + line: node.startPosition.row + 1, + }); + } + } + } +} + +function handleCppStructSpecifier(node: TreeSitterNode, ctx: ExtractorOutput): void { + const nameNode = node.childForFieldName('name'); + if (!nameNode) return; + const children = extractCppClassFields(node); + ctx.definitions.push({ + name: nameNode.text, + kind: 'struct', + line: node.startPosition.row + 1, + endLine: nodeEndLine(node), + children: children.length > 0 ? children : undefined, + }); + + const baseClause = findChild(node, 'base_class_clause'); + if (baseClause) { + for (let i = 0; i < baseClause.childCount; i++) { + const child = baseClause.child(i); + if (child && (child.type === 'type_identifier' || child.type === 'qualified_identifier')) { + ctx.classes.push({ + name: nameNode.text, + extends: child.text, + line: node.startPosition.row + 1, + }); + } + } + } +} + +function handleCppEnumSpecifier(node: TreeSitterNode, ctx: ExtractorOutput): void { + const nameNode = node.childForFieldName('name'); + if (!nameNode) return; + const children = extractCppEnumEntries(node); + ctx.definitions.push({ + name: nameNode.text, + kind: 'enum', + line: node.startPosition.row + 1, + endLine: nodeEndLine(node), + children: children.length > 0 ? children : undefined, + }); +} + +function handleCppNamespaceDef(node: TreeSitterNode, ctx: ExtractorOutput): void { + const nameNode = node.childForFieldName('name'); + if (!nameNode) return; + ctx.definitions.push({ + name: nameNode.text, + kind: 'namespace', + line: node.startPosition.row + 1, + endLine: nodeEndLine(node), + }); +} + +function handleCppTypedef(node: TreeSitterNode, ctx: ExtractorOutput): void { + let name: string | undefined; + for (let i = node.childCount - 1; i >= 0; i--) { + const child = node.child(i); + if ( + child && + (child.type === 'type_identifier' || + child.type === 'identifier' || + child.type === 'primitive_type') + ) { + name = child.text; + break; + } + } + if (!name) return; + ctx.definitions.push({ + name, + kind: 'type', + line: node.startPosition.row + 1, + endLine: nodeEndLine(node), + }); +} + +function handleCppInclude(node: TreeSitterNode, ctx: ExtractorOutput): void { + const pathNode = node.childForFieldName('path'); + if (!pathNode) return; + const raw = pathNode.text; + const source = raw.replace(/^["<]|[">]$/g, ''); + const lastName = source.split('/').pop() ?? source; + ctx.imports.push({ + source, + names: [lastName], + line: node.startPosition.row + 1, + cInclude: true, + }); +} + +function handleCppCallExpression(node: TreeSitterNode, ctx: ExtractorOutput): void { + const funcNode = node.childForFieldName('function'); + if (!funcNode) return; + const call: Call = { name: '', line: node.startPosition.row + 1 }; + if (funcNode.type === 'field_expression') { + const field = funcNode.childForFieldName('field'); + const argument = funcNode.childForFieldName('argument'); + if (field) call.name = field.text; + if (argument) call.receiver = argument.text; + } else { + call.name = funcNode.text; + } + if (call.name) ctx.calls.push(call); +} + +// ── Utility helpers ───────────────────────────────────────────────────────── + +function findCppParentClass(node: TreeSitterNode): string | null { + let current = node.parent; + while (current) { + if (current.type === 'field_declaration_list') { + const classNode = current.parent; + if ( + classNode && + (classNode.type === 'class_specifier' || classNode.type === 'struct_specifier') + ) { + const nameNode = classNode.childForFieldName('name'); + return nameNode ? nameNode.text : null; + } + } + current = current.parent; + } + return null; +} + +function extractCppParameters(paramListNode: TreeSitterNode | null): SubDeclaration[] { + const params: SubDeclaration[] = []; + if (!paramListNode) return params; + for (let i = 0; i < paramListNode.childCount; i++) { + const param = paramListNode.child(i); + if (!param || param.type !== 'parameter_declaration') continue; + const nameNode = param.childForFieldName('declarator'); + if (nameNode) { + const name = + nameNode.type === 'identifier' + ? nameNode.text + : (findChild(nameNode, 'identifier')?.text ?? nameNode.text); + params.push({ name, kind: 'parameter', line: param.startPosition.row + 1 }); + } + } + return params; +} + +function extractCppClassFields(classNode: TreeSitterNode): SubDeclaration[] { + const fields: SubDeclaration[] = []; + const body = + classNode.childForFieldName('body') || findChild(classNode, 'field_declaration_list'); + if (!body) return fields; + for (let i = 0; i < body.childCount; i++) { + const member = body.child(i); + if (!member || member.type !== 'field_declaration') continue; + const nameNode = member.childForFieldName('declarator'); + if (nameNode) { + const name = + nameNode.type === 'identifier' + ? nameNode.text + : (findChild(nameNode, 'identifier')?.text ?? nameNode.text); + fields.push({ + name, + kind: 'property', + line: member.startPosition.row + 1, + visibility: extractModifierVisibility(member), + }); + } + } + return fields; +} + +function extractCppEnumEntries(enumNode: TreeSitterNode): SubDeclaration[] { + const entries: SubDeclaration[] = []; + const body = findChild(enumNode, 'enumerator_list'); + if (!body) return entries; + for (let i = 0; i < body.childCount; i++) { + const member = body.child(i); + if (!member || member.type !== 'enumerator') continue; + const nameNode = member.childForFieldName('name'); + if (nameNode) { + entries.push({ name: nameNode.text, kind: 'constant', line: member.startPosition.row + 1 }); + } + } + return entries; +} diff --git a/src/extractors/index.ts b/src/extractors/index.ts index 4d26db5c..65b7e1d9 100644 --- a/src/extractors/index.ts +++ b/src/extractors/index.ts @@ -1,9 +1,15 @@ +export { extractBashSymbols } from './bash.js'; +export { extractCSymbols } from './c.js'; +export { extractCppSymbols } from './cpp.js'; export { extractCSharpSymbols } from './csharp.js'; export { extractGoSymbols } from './go.js'; export { extractHCLSymbols } from './hcl.js'; export { extractJavaSymbols } from './java.js'; export { extractSymbols } from './javascript.js'; +export { extractKotlinSymbols } from './kotlin.js'; export { extractPHPSymbols } from './php.js'; export { extractPythonSymbols } from './python.js'; export { extractRubySymbols } from './ruby.js'; export { extractRustSymbols } from './rust.js'; +export { extractScalaSymbols } from './scala.js'; +export { extractSwiftSymbols } from './swift.js'; diff --git a/src/extractors/kotlin.ts b/src/extractors/kotlin.ts new file mode 100644 index 00000000..6575beef --- /dev/null +++ b/src/extractors/kotlin.ts @@ -0,0 +1,293 @@ +import type { + Call, + ExtractorOutput, + SubDeclaration, + TreeSitterNode, + TreeSitterTree, +} from '../types.js'; +import { extractModifierVisibility, findChild, nodeEndLine } from './helpers.js'; + +/** + * Extract symbols from Kotlin files. + */ +export function extractKotlinSymbols(tree: TreeSitterTree, _filePath: string): ExtractorOutput { + const ctx: ExtractorOutput = { + definitions: [], + calls: [], + imports: [], + classes: [], + exports: [], + typeMap: new Map(), + }; + + walkKotlinNode(tree.rootNode, ctx); + return ctx; +} + +function walkKotlinNode(node: TreeSitterNode, ctx: ExtractorOutput): void { + switch (node.type) { + case 'class_declaration': + handleKotlinClassDecl(node, ctx); + break; + case 'object_declaration': + handleKotlinObjectDecl(node, ctx); + break; + case 'function_declaration': + handleKotlinFunctionDecl(node, ctx); + break; + case 'import_header': + handleKotlinImport(node, ctx); + break; + case 'call_expression': + handleKotlinCallExpression(node, ctx); + break; + case 'navigation_expression': + handleKotlinNavExpression(node, ctx); + break; + } + + for (let i = 0; i < node.childCount; i++) { + const child = node.child(i); + if (child) walkKotlinNode(child, ctx); + } +} + +// ── Walk-path per-node-type handlers ──────────────────────────────────────── + +function hasKeywordChild(node: TreeSitterNode, keyword: string): boolean { + for (let i = 0; i < node.childCount; i++) { + const child = node.child(i); + if (child && child.text === keyword) return true; + } + return false; +} + +function hasModifier(node: TreeSitterNode, keyword: string): boolean { + for (let i = 0; i < node.childCount; i++) { + const child = node.child(i); + if (!child) continue; + if (child.type === 'modifiers' && child.text.includes(keyword)) return true; + } + return false; +} + +function handleKotlinClassDecl(node: TreeSitterNode, ctx: ExtractorOutput): void { + const isInterface = hasKeywordChild(node, 'interface'); + const isEnum = hasModifier(node, 'enum'); + + const nameNode = findChild(node, 'type_identifier'); + if (!nameNode) return; + const name = nameNode.text; + + const kind = isInterface ? 'interface' : isEnum ? 'enum' : 'class'; + + const children: SubDeclaration[] = []; + if (isEnum) { + // Enum entries are inside class_body + const body = findChild(node, 'class_body'); + if (body) { + for (let i = 0; i < body.childCount; i++) { + const child = body.child(i); + if (child && child.type === 'enum_entry') { + const entryName = findChild(child, 'simple_identifier'); + if (entryName) { + children.push({ + name: entryName.text, + kind: 'constant', + line: child.startPosition.row + 1, + }); + } + } + } + } + } else { + // Extract properties from class_body + const body = findChild(node, 'class_body'); + if (body) { + for (let i = 0; i < body.childCount; i++) { + const child = body.child(i); + if (child && child.type === 'property_declaration') { + const propName = findChild(child, 'variable_declaration'); + if (propName) { + const id = findChild(propName, 'simple_identifier'); + if (id) { + children.push({ + name: id.text, + kind: 'property', + line: child.startPosition.row + 1, + visibility: extractModifierVisibility(child), + }); + } + } + } + } + } + } + + ctx.definitions.push({ + name, + kind, + line: node.startPosition.row + 1, + endLine: nodeEndLine(node), + children: children.length > 0 ? children : undefined, + }); + + // Methods inside class_body + const body = findChild(node, 'class_body'); + if (body) { + for (let i = 0; i < body.childCount; i++) { + const child = body.child(i); + if (child && child.type === 'function_declaration') { + const methName = findChild(child, 'simple_identifier'); + if (methName) { + ctx.definitions.push({ + name: `${name}.${methName.text}`, + kind: 'method', + line: child.startPosition.row + 1, + endLine: child.endPosition.row + 1, + visibility: extractModifierVisibility(child), + }); + } + } + } + } + + // Inheritance: delegation_specifier nodes are DIRECT children + for (let i = 0; i < node.childCount; i++) { + const child = node.child(i); + if (!child || child.type !== 'delegation_specifier') continue; + + // constructor_invocation > user_type > type_identifier (extends) + const ctorInvocation = findChild(child, 'constructor_invocation'); + if (ctorInvocation) { + const userType = findChild(ctorInvocation, 'user_type'); + if (userType) { + const typeId = findChild(userType, 'type_identifier'); + if (typeId) { + ctx.classes.push({ + name, + extends: typeId.text, + line: node.startPosition.row + 1, + }); + } + } + continue; + } + + // user_type > type_identifier (implements) + const userType = findChild(child, 'user_type'); + if (userType) { + const typeId = findChild(userType, 'type_identifier'); + if (typeId) { + ctx.classes.push({ + name, + implements: typeId.text, + line: node.startPosition.row + 1, + }); + } + } + } +} + +function handleKotlinObjectDecl(node: TreeSitterNode, ctx: ExtractorOutput): void { + const nameNode = findChild(node, 'type_identifier'); + if (!nameNode) return; + ctx.definitions.push({ + name: nameNode.text, + kind: 'class', + line: node.startPosition.row + 1, + endLine: nodeEndLine(node), + }); + + // Methods inside object body + const body = findChild(node, 'class_body'); + if (body) { + for (let i = 0; i < body.childCount; i++) { + const child = body.child(i); + if (child && child.type === 'function_declaration') { + const methName = findChild(child, 'simple_identifier'); + if (methName) { + ctx.definitions.push({ + name: `${nameNode.text}.${methName.text}`, + kind: 'method', + line: child.startPosition.row + 1, + endLine: child.endPosition.row + 1, + visibility: extractModifierVisibility(child), + }); + } + } + } + } +} + +function handleKotlinFunctionDecl(node: TreeSitterNode, ctx: ExtractorOutput): void { + // Skip methods already emitted by class/object handlers + if ( + node.parent?.type === 'class_body' && + (node.parent.parent?.type === 'class_declaration' || + node.parent.parent?.type === 'object_declaration') + ) { + return; + } + const nameNode = findChild(node, 'simple_identifier'); + if (!nameNode) return; + const params = extractKotlinParameters(node); + ctx.definitions.push({ + name: nameNode.text, + kind: 'function', + line: node.startPosition.row + 1, + endLine: nodeEndLine(node), + children: params.length > 0 ? params : undefined, + visibility: extractModifierVisibility(node), + }); +} + +function handleKotlinImport(node: TreeSitterNode, ctx: ExtractorOutput): void { + const identNode = findChild(node, 'identifier'); + if (!identNode) return; + const fullPath = identNode.text; + const lastName = fullPath.split('.').pop() ?? fullPath; + ctx.imports.push({ + source: fullPath, + names: [lastName], + line: node.startPosition.row + 1, + kotlinImport: true, + }); +} + +function handleKotlinCallExpression(node: TreeSitterNode, ctx: ExtractorOutput): void { + const funcNode = node.child(0); + if (!funcNode) return; + if (funcNode.type === 'simple_identifier') { + ctx.calls.push({ name: funcNode.text, line: node.startPosition.row + 1 }); + } +} + +function handleKotlinNavExpression(node: TreeSitterNode, ctx: ExtractorOutput): void { + // navigation_expression: expr . identifier — only emit if parent is call_expression + if (node.parent?.type !== 'call_expression') return; + const lastChild = node.child(node.childCount - 1); + const firstChild = node.child(0); + if (lastChild && lastChild.type === 'simple_identifier' && firstChild) { + const call: Call = { name: lastChild.text, line: node.startPosition.row + 1 }; + call.receiver = firstChild.text; + ctx.calls.push(call); + } +} + +// ── Child extraction helpers ──────────────────────────────────────────────── + +function extractKotlinParameters(funcNode: TreeSitterNode): SubDeclaration[] { + const params: SubDeclaration[] = []; + const paramList = findChild(funcNode, 'function_value_parameters'); + if (!paramList) return params; + for (let i = 0; i < paramList.childCount; i++) { + const param = paramList.child(i); + if (!param || param.type !== 'parameter') continue; + const nameNode = findChild(param, 'simple_identifier'); + if (nameNode) { + params.push({ name: nameNode.text, kind: 'parameter', line: param.startPosition.row + 1 }); + } + } + return params; +} diff --git a/src/extractors/scala.ts b/src/extractors/scala.ts new file mode 100644 index 00000000..a85f05b8 --- /dev/null +++ b/src/extractors/scala.ts @@ -0,0 +1,285 @@ +import type { + Call, + ExtractorOutput, + SubDeclaration, + TreeSitterNode, + TreeSitterTree, +} from '../types.js'; +import { extractModifierVisibility, findChild, nodeEndLine } from './helpers.js'; + +/** + * Extract symbols from Scala files. + */ +export function extractScalaSymbols(tree: TreeSitterTree, _filePath: string): ExtractorOutput { + const ctx: ExtractorOutput = { + definitions: [], + calls: [], + imports: [], + classes: [], + exports: [], + typeMap: new Map(), + }; + + walkScalaNode(tree.rootNode, ctx); + return ctx; +} + +function walkScalaNode(node: TreeSitterNode, ctx: ExtractorOutput): void { + switch (node.type) { + case 'class_definition': + handleScalaClassDef(node, ctx); + break; + case 'trait_definition': + handleScalaTraitDef(node, ctx); + break; + case 'object_definition': + handleScalaObjectDef(node, ctx); + break; + case 'function_definition': + handleScalaFunctionDef(node, ctx); + break; + case 'import_declaration': + handleScalaImportDecl(node, ctx); + break; + case 'call_expression': + handleScalaCallExpression(node, ctx); + break; + case 'val_definition': + case 'var_definition': + handleScalaValVarDef(node, ctx); + break; + } + + for (let i = 0; i < node.childCount; i++) { + const child = node.child(i); + if (child) walkScalaNode(child, ctx); + } +} + +// ── Walk-path per-node-type handlers ──────────────────────────────────────── + +function handleScalaClassDef(node: TreeSitterNode, ctx: ExtractorOutput): void { + const nameNode = node.childForFieldName('name'); + if (!nameNode) return; + const name = nameNode.text; + const children = extractScalaBodyMembers(node, name, ctx); + + ctx.definitions.push({ + name, + kind: 'class', + line: node.startPosition.row + 1, + endLine: nodeEndLine(node), + children: children.length > 0 ? children : undefined, + }); + + extractScalaInheritance(node, name, ctx); +} + +function handleScalaTraitDef(node: TreeSitterNode, ctx: ExtractorOutput): void { + const nameNode = node.childForFieldName('name'); + if (!nameNode) return; + const name = nameNode.text; + const children = extractScalaBodyMembers(node, name, ctx); + + ctx.definitions.push({ + name, + kind: 'interface', + line: node.startPosition.row + 1, + endLine: nodeEndLine(node), + children: children.length > 0 ? children : undefined, + }); + + extractScalaInheritance(node, name, ctx); +} + +function handleScalaObjectDef(node: TreeSitterNode, ctx: ExtractorOutput): void { + const nameNode = node.childForFieldName('name'); + if (!nameNode) return; + const name = nameNode.text; + const children = extractScalaBodyMembers(node, name, ctx); + + ctx.definitions.push({ + name, + kind: 'class', + line: node.startPosition.row + 1, + endLine: nodeEndLine(node), + children: children.length > 0 ? children : undefined, + }); + + extractScalaInheritance(node, name, ctx); +} + +function handleScalaFunctionDef(node: TreeSitterNode, ctx: ExtractorOutput): void { + // Skip methods already emitted by class/trait/object handlers + if (node.parent?.type === 'template_body') { + const grandparent = node.parent.parent; + if ( + grandparent && + (grandparent.type === 'class_definition' || + grandparent.type === 'trait_definition' || + grandparent.type === 'object_definition') + ) { + return; + } + } + const nameNode = node.childForFieldName('name'); + if (!nameNode) return; + const params = extractScalaParameters(node); + ctx.definitions.push({ + name: nameNode.text, + kind: 'function', + line: node.startPosition.row + 1, + endLine: nodeEndLine(node), + children: params.length > 0 ? params : undefined, + visibility: extractModifierVisibility(node), + }); +} + +function handleScalaImportDecl(node: TreeSitterNode, ctx: ExtractorOutput): void { + // import_declaration has alternating `identifier` and `.` children directly (NO import_expression wrapper) + const parts: string[] = []; + for (let i = 0; i < node.childCount; i++) { + const child = node.child(i); + if (!child) continue; + if (child.type === 'identifier' || child.type === 'type_identifier') { + parts.push(child.text); + } + } + if (parts.length === 0) return; + const fullPath = parts.join('.'); + const lastName = parts[parts.length - 1] ?? fullPath; + ctx.imports.push({ + source: fullPath, + names: [lastName], + line: node.startPosition.row + 1, + scalaImport: true, + }); +} + +function handleScalaCallExpression(node: TreeSitterNode, ctx: ExtractorOutput): void { + const funcNode = node.childForFieldName('function'); + if (!funcNode) return; + const call: Call = { name: '', line: node.startPosition.row + 1 }; + if (funcNode.type === 'field_expression') { + const field = funcNode.childForFieldName('field'); + const value = funcNode.childForFieldName('value'); + if (field) call.name = field.text; + if (value) call.receiver = value.text; + } else { + call.name = funcNode.text; + } + if (call.name) ctx.calls.push(call); +} + +function handleScalaValVarDef(node: TreeSitterNode, ctx: ExtractorOutput): void { + // Only handle top-level vals/vars — skip class members and function-local bindings + if (node.parent?.type === 'template_body') return; + if (node.parent?.type === 'block' || node.parent?.type === 'indented_block') return; + const pattern = node.childForFieldName('pattern'); + if (!pattern) return; + const nameNode = pattern.type === 'identifier' ? pattern : findChild(pattern, 'identifier'); + if (!nameNode) return; + const kind = node.type === 'val_definition' ? 'constant' : 'variable'; + ctx.definitions.push({ + name: nameNode.text, + kind, + line: node.startPosition.row + 1, + endLine: nodeEndLine(node), + }); +} + +// ── Inheritance helpers ───────────────────────────────────────────────────── + +function extractScalaInheritance(node: TreeSitterNode, name: string, ctx: ExtractorOutput): void { + const extendsClause = findChild(node, 'extends_clause'); + if (!extendsClause) return; + let foundExtends = false; + for (let i = 0; i < extendsClause.childCount; i++) { + const child = extendsClause.child(i); + if (!child) continue; + if ( + child.type === 'type_identifier' || + child.type === 'generic_type' || + child.type === 'identifier' + ) { + const typeName = child.type === 'generic_type' ? child.child(0)?.text : child.text; + if (!typeName) continue; + if (!foundExtends) { + ctx.classes.push({ + name, + extends: typeName, + line: node.startPosition.row + 1, + }); + foundExtends = true; + } else { + ctx.classes.push({ + name, + implements: typeName, + line: node.startPosition.row + 1, + }); + } + } + } +} + +// ── Body member extraction ────────────────────────────────────────────────── + +function extractScalaBodyMembers( + parentNode: TreeSitterNode, + parentName: string, + ctx: ExtractorOutput, +): SubDeclaration[] { + const children: SubDeclaration[] = []; + const body = findChild(parentNode, 'template_body'); + if (!body) return children; + + for (let i = 0; i < body.childCount; i++) { + const member = body.child(i); + if (!member) continue; + + if (member.type === 'function_definition') { + const methName = member.childForFieldName('name'); + if (methName) { + ctx.definitions.push({ + name: `${parentName}.${methName.text}`, + kind: 'method', + line: member.startPosition.row + 1, + endLine: member.endPosition.row + 1, + visibility: extractModifierVisibility(member), + }); + } + } else if (member.type === 'val_definition' || member.type === 'var_definition') { + const pattern = member.childForFieldName('pattern'); + if (pattern) { + const nameNode = pattern.type === 'identifier' ? pattern : findChild(pattern, 'identifier'); + if (nameNode) { + children.push({ + name: nameNode.text, + kind: 'property', + line: member.startPosition.row + 1, + visibility: extractModifierVisibility(member), + }); + } + } + } + } + + return children; +} + +// ── Parameter extraction ──────────────────────────────────────────────────── + +function extractScalaParameters(funcNode: TreeSitterNode): SubDeclaration[] { + const params: SubDeclaration[] = []; + const paramList = findChild(funcNode, 'parameters'); + if (!paramList) return params; + for (let i = 0; i < paramList.childCount; i++) { + const param = paramList.child(i); + if (!param || param.type !== 'parameter') continue; + const nameNode = findChild(param, 'identifier'); + if (nameNode) { + params.push({ name: nameNode.text, kind: 'parameter', line: param.startPosition.row + 1 }); + } + } + return params; +} diff --git a/src/extractors/swift.ts b/src/extractors/swift.ts new file mode 100644 index 00000000..c697bf43 --- /dev/null +++ b/src/extractors/swift.ts @@ -0,0 +1,293 @@ +import type { + Call, + ExtractorOutput, + SubDeclaration, + TreeSitterNode, + TreeSitterTree, +} from '../types.js'; +import { extractModifierVisibility, findChild, nodeEndLine } from './helpers.js'; + +/** + * Extract symbols from Swift files. + */ +export function extractSwiftSymbols(tree: TreeSitterTree, _filePath: string): ExtractorOutput { + const ctx: ExtractorOutput = { + definitions: [], + calls: [], + imports: [], + classes: [], + exports: [], + typeMap: new Map(), + }; + + walkSwiftNode(tree.rootNode, ctx); + return ctx; +} + +function walkSwiftNode(node: TreeSitterNode, ctx: ExtractorOutput): void { + switch (node.type) { + case 'class_declaration': + handleSwiftClassDecl(node, ctx); + break; + case 'protocol_declaration': + handleSwiftProtocolDecl(node, ctx); + break; + case 'function_declaration': + handleSwiftFunctionDecl(node, ctx); + break; + case 'import_declaration': + handleSwiftImportDecl(node, ctx); + break; + case 'call_expression': + handleSwiftCallExpression(node, ctx); + break; + case 'property_declaration': + handleSwiftPropertyDecl(node, ctx); + break; + } + + for (let i = 0; i < node.childCount; i++) { + const child = node.child(i); + if (child) walkSwiftNode(child, ctx); + } +} + +// ── Walk-path per-node-type handlers ──────────────────────────────────────── + +function hasKeywordChild(node: TreeSitterNode, keyword: string): boolean { + for (let i = 0; i < node.childCount; i++) { + const child = node.child(i); + if (child && child.text === keyword) return true; + } + return false; +} + +function handleSwiftClassDecl(node: TreeSitterNode, ctx: ExtractorOutput): void { + const isStruct = hasKeywordChild(node, 'struct'); + const isEnum = hasKeywordChild(node, 'enum'); + + // Name is a type_identifier direct child + const nameNode = findChild(node, 'type_identifier'); + if (!nameNode) return; + const name = nameNode.text; + + const kind = isEnum ? 'enum' : isStruct ? 'struct' : 'class'; + + const children: SubDeclaration[] = []; + + if (isEnum) { + // Enum cases: enum_entry > simple_identifier, inside enum_class_body + const body = findChild(node, 'enum_class_body'); + if (body) { + for (let i = 0; i < body.childCount; i++) { + const child = body.child(i); + if (child && child.type === 'enum_entry') { + const entryName = findChild(child, 'simple_identifier'); + if (entryName) { + children.push({ + name: entryName.text, + kind: 'constant', + line: child.startPosition.row + 1, + }); + } + } + } + } + } else { + // Extract properties from class_body + const body = findChild(node, 'class_body'); + if (body) { + for (let i = 0; i < body.childCount; i++) { + const child = body.child(i); + if (child && child.type === 'property_declaration') { + const pattern = findChild(child, 'pattern'); + if (pattern) { + const propName = findChild(pattern, 'simple_identifier'); + if (propName) { + children.push({ + name: propName.text, + kind: 'property', + line: child.startPosition.row + 1, + visibility: extractModifierVisibility(child), + }); + } + } + } + } + } + } + + ctx.definitions.push({ + name, + kind, + line: node.startPosition.row + 1, + endLine: nodeEndLine(node), + children: children.length > 0 ? children : undefined, + }); + + // Methods inside class_body or enum_class_body + const body = findChild(node, 'class_body') || findChild(node, 'enum_class_body'); + if (body) { + for (let i = 0; i < body.childCount; i++) { + const child = body.child(i); + if (child && child.type === 'function_declaration') { + const methName = findChild(child, 'simple_identifier'); + if (methName) { + ctx.definitions.push({ + name: `${name}.${methName.text}`, + kind: 'method', + line: child.startPosition.row + 1, + endLine: child.endPosition.row + 1, + visibility: extractModifierVisibility(child), + }); + } + } + } + } + + // Inheritance: inheritance_specifier nodes are DIRECT children of class_declaration + // First specifier is the superclass (extends), rest are protocol conformances (implements) + let first = true; + for (let i = 0; i < node.childCount; i++) { + const child = node.child(i); + if (!child || child.type !== 'inheritance_specifier') continue; + // inheritance_specifier > user_type > type_identifier + const userType = findChild(child, 'user_type'); + if (userType) { + const typeId = findChild(userType, 'type_identifier'); + if (typeId) { + if (first) { + ctx.classes.push({ + name, + extends: typeId.text, + line: node.startPosition.row + 1, + }); + first = false; + } else { + ctx.classes.push({ + name, + implements: typeId.text, + line: node.startPosition.row + 1, + }); + } + } + } + } +} + +function handleSwiftProtocolDecl(node: TreeSitterNode, ctx: ExtractorOutput): void { + const nameNode = findChild(node, 'type_identifier'); + if (!nameNode) return; + const name = nameNode.text; + + ctx.definitions.push({ + name, + kind: 'interface', + line: node.startPosition.row + 1, + endLine: nodeEndLine(node), + }); + + // Methods inside protocol_body or class_body + const body = findChild(node, 'protocol_body') || findChild(node, 'class_body'); + if (body) { + for (let i = 0; i < body.childCount; i++) { + const child = body.child(i); + if (child && child.type === 'function_declaration') { + const methName = findChild(child, 'simple_identifier'); + if (methName) { + ctx.definitions.push({ + name: `${name}.${methName.text}`, + kind: 'method', + line: child.startPosition.row + 1, + endLine: child.endPosition.row + 1, + }); + } + } + } + } +} + +function handleSwiftFunctionDecl(node: TreeSitterNode, ctx: ExtractorOutput): void { + // Skip methods already emitted by class/protocol handlers + if ( + node.parent?.type === 'class_body' || + node.parent?.type === 'protocol_body' || + node.parent?.type === 'enum_class_body' + ) { + if ( + node.parent.parent?.type === 'class_declaration' || + node.parent.parent?.type === 'protocol_declaration' + ) { + return; + } + } + const nameNode = findChild(node, 'simple_identifier'); + if (!nameNode) return; + ctx.definitions.push({ + name: nameNode.text, + kind: 'function', + line: node.startPosition.row + 1, + endLine: nodeEndLine(node), + visibility: extractModifierVisibility(node), + }); +} + +function handleSwiftImportDecl(node: TreeSitterNode, ctx: ExtractorOutput): void { + const identNode = findChild(node, 'identifier'); + if (!identNode) return; + const source = identNode.text; + ctx.imports.push({ + source, + names: [source], + line: node.startPosition.row + 1, + swiftImport: true, + }); +} + +function handleSwiftCallExpression(node: TreeSitterNode, ctx: ExtractorOutput): void { + const funcNode = node.child(0); + if (!funcNode) return; + const call: Call = { name: '', line: node.startPosition.row + 1 }; + if (funcNode.type === 'navigation_expression') { + // obj.method(...) + const lastChild = funcNode.child(funcNode.childCount - 1); + const firstChild = funcNode.child(0); + if (lastChild && lastChild.type === 'simple_identifier' && firstChild) { + call.name = lastChild.text; + call.receiver = firstChild.text; + } + } else if (funcNode.type === 'simple_identifier') { + call.name = funcNode.text; + } else { + call.name = funcNode.text; + } + if (call.name) ctx.calls.push(call); +} + +function handleSwiftPropertyDecl(node: TreeSitterNode, ctx: ExtractorOutput): void { + // Only handle top-level properties (class properties are handled inline) + if ( + node.parent?.type === 'class_body' || + node.parent?.type === 'protocol_body' || + node.parent?.type === 'enum_class_body' + ) { + return; + } + // Skip function-local let/var bindings + if (node.parent?.type === 'statements' || node.parent?.type === 'function_body') { + return; + } + const pattern = findChild(node, 'pattern'); + if (!pattern) return; + const nameNode = findChild(pattern, 'simple_identifier'); + if (!nameNode) return; + // let → constant, var → variable + const isLet = hasKeywordChild(node, 'let'); + const kind = isLet ? 'constant' : 'variable'; + ctx.definitions.push({ + name: nameNode.text, + kind, + line: node.startPosition.row + 1, + endLine: nodeEndLine(node), + }); +} diff --git a/src/presentation/colors.ts b/src/presentation/colors.ts index 57171d80..55a714c5 100644 --- a/src/presentation/colors.ts +++ b/src/presentation/colors.ts @@ -23,6 +23,8 @@ export const DEFAULT_NODE_COLORS: Record = { parameter: '#B0BEC5', property: '#B0BEC5', constant: '#B0BEC5', + namespace: '#78909C', + variable: '#B0BEC5', }; export const DEFAULT_ROLE_COLORS: Partial> = { diff --git a/src/types.ts b/src/types.ts index f9b460e4..2a0751f5 100644 --- a/src/types.ts +++ b/src/types.ts @@ -22,10 +22,11 @@ export type CoreSymbolKind = | 'enum' | 'trait' | 'record' - | 'module'; + | 'module' + | 'namespace'; /** Sub-declaration kinds (Phase 1). Includes 'method' for class child nodes. */ -export type ExtendedSymbolKind = 'parameter' | 'property' | 'constant' | 'method'; +export type ExtendedSymbolKind = 'parameter' | 'property' | 'constant' | 'variable' | 'method'; /** All queryable symbol kinds. */ export type SymbolKind = CoreSymbolKind | ExtendedSymbolKind; @@ -83,7 +84,13 @@ export type LanguageId = | 'csharp' | 'ruby' | 'php' - | 'hcl'; + | 'hcl' + | 'c' + | 'cpp' + | 'kotlin' + | 'swift' + | 'scala' + | 'bash'; /** Engine mode selector. */ export type EngineMode = 'native' | 'wasm' | 'auto'; @@ -433,6 +440,11 @@ export interface Import { csharpUsing?: boolean; rubyRequire?: boolean; phpUse?: boolean; + cInclude?: boolean; + kotlinImport?: boolean; + swiftImport?: boolean; + scalaImport?: boolean; + bashSource?: boolean; } /** A class/struct/trait relationship (extends or implements). */ diff --git a/tests/parsers/bash.test.ts b/tests/parsers/bash.test.ts new file mode 100644 index 00000000..c67bb412 --- /dev/null +++ b/tests/parsers/bash.test.ts @@ -0,0 +1,47 @@ +import { beforeAll, describe, expect, it } from 'vitest'; +import { createParsers, extractBashSymbols } from '../../src/domain/parser.js'; + +describe('Bash parser', () => { + let parsers: any; + + beforeAll(async () => { + parsers = await createParsers(); + }); + + function parseBash(code) { + const parser = parsers.get('bash'); + if (!parser) throw new Error('Bash parser not available'); + const tree = parser.parse(code); + return extractBashSymbols(tree, 'test.sh'); + } + + it('extracts function definitions', () => { + const symbols = parseBash(`function greet() { echo "hello"; }`); + expect(symbols.definitions).toContainEqual( + expect.objectContaining({ name: 'greet', kind: 'function' }), + ); + }); + + it('extracts function definitions (shorthand)', () => { + const symbols = parseBash(`greet() { echo "hello"; }`); + expect(symbols.definitions).toContainEqual( + expect.objectContaining({ name: 'greet', kind: 'function' }), + ); + }); + + it('extracts command calls', () => { + const symbols = parseBash(`#!/bin/bash\necho "hello"\nls -la`); + expect(symbols.calls).toContainEqual(expect.objectContaining({ name: 'echo' })); + expect(symbols.calls).toContainEqual(expect.objectContaining({ name: 'ls' })); + }); + + it('extracts source imports', () => { + const symbols = parseBash(`source ./utils.sh`); + expect(symbols.imports).toContainEqual(expect.objectContaining({ bashSource: true })); + }); + + it('extracts dot source imports', () => { + const symbols = parseBash(`. ./config.sh`); + expect(symbols.imports).toContainEqual(expect.objectContaining({ bashSource: true })); + }); +}); diff --git a/tests/parsers/c.test.ts b/tests/parsers/c.test.ts new file mode 100644 index 00000000..e4b1b9df --- /dev/null +++ b/tests/parsers/c.test.ts @@ -0,0 +1,73 @@ +import { beforeAll, describe, expect, it } from 'vitest'; +import { createParsers, extractCSymbols } from '../../src/domain/parser.js'; + +describe('C parser', () => { + let parsers: any; + + beforeAll(async () => { + parsers = await createParsers(); + }); + + function parseC(code) { + const parser = parsers.get('c'); + if (!parser) throw new Error('C parser not available'); + const tree = parser.parse(code); + return extractCSymbols(tree, 'test.c'); + } + + it('extracts function definitions', () => { + const symbols = parseC(`int main(int argc, char **argv) { return 0; }`); + const main = symbols.definitions.find((d) => d.name === 'main'); + expect(main).toBeDefined(); + expect(main.kind).toBe('function'); + expect(main.children).toBeDefined(); + expect(main.children.length).toBe(2); + expect(main.children[0].name).toBe('argc'); + expect(main.children[0].kind).toBe('parameter'); + }); + + it('extracts struct definitions', () => { + const symbols = parseC(`struct Point { int x; int y; };`); + expect(symbols.definitions).toContainEqual( + expect.objectContaining({ name: 'Point', kind: 'struct' }), + ); + }); + + it('extracts enum definitions', () => { + const symbols = parseC(`enum Color { RED, GREEN, BLUE };`); + const color = symbols.definitions.find((d) => d.name === 'Color'); + expect(color).toBeDefined(); + expect(color.kind).toBe('enum'); + expect(color.children).toBeDefined(); + expect(color.children.length).toBe(3); + expect(color.children[0].name).toBe('RED'); + expect(color.children[0].kind).toBe('constant'); + }); + + it('extracts typedef', () => { + const symbols = parseC(`typedef unsigned long size_t;`); + expect(symbols.definitions).toContainEqual( + expect.objectContaining({ name: 'size_t', kind: 'type' }), + ); + }); + + it('extracts includes', () => { + const symbols = parseC(`#include \n#include "mylib.h"`); + expect(symbols.imports.length).toBe(2); + expect(symbols.imports[0].source).toBe('stdio.h'); + expect(symbols.imports[0].cInclude).toBe(true); + expect(symbols.imports[1].source).toBe('mylib.h'); + }); + + it('extracts function calls', () => { + const symbols = parseC(`void f() { printf("hello"); }`); + expect(symbols.calls).toContainEqual(expect.objectContaining({ name: 'printf' })); + }); + + it('extracts calls with receiver', () => { + const symbols = parseC(`void f() { obj->method(); }`); + const call = symbols.calls.find((c) => c.name === 'method'); + expect(call).toBeDefined(); + expect(call.receiver).toBe('obj'); + }); +}); diff --git a/tests/parsers/cpp.test.ts b/tests/parsers/cpp.test.ts new file mode 100644 index 00000000..d1753225 --- /dev/null +++ b/tests/parsers/cpp.test.ts @@ -0,0 +1,76 @@ +import { beforeAll, describe, expect, it } from 'vitest'; +import { createParsers, extractCppSymbols } from '../../src/domain/parser.js'; + +describe('C++ parser', () => { + let parsers: any; + + beforeAll(async () => { + parsers = await createParsers(); + }); + + function parseCpp(code) { + const parser = parsers.get('cpp'); + if (!parser) throw new Error('C++ parser not available'); + const tree = parser.parse(code); + return extractCppSymbols(tree, 'test.cpp'); + } + + it('extracts class declarations', () => { + const symbols = parseCpp(`class Animal { };`); + expect(symbols.definitions).toContainEqual( + expect.objectContaining({ name: 'Animal', kind: 'class', line: 1 }), + ); + }); + + it('extracts class with methods', () => { + const symbols = parseCpp(`class Animal { + void speak() { } +};`); + expect(symbols.definitions).toContainEqual( + expect.objectContaining({ name: 'Animal', kind: 'class' }), + ); + expect(symbols.definitions).toContainEqual( + expect.objectContaining({ name: 'Animal.speak', kind: 'method' }), + ); + }); + + it('extracts inheritance', () => { + const symbols = parseCpp(`class Dog : public Animal { };`); + expect(symbols.classes).toContainEqual( + expect.objectContaining({ name: 'Dog', extends: 'Animal' }), + ); + }); + + it('extracts namespace', () => { + const symbols = parseCpp(`namespace utils { void helper() { } }`); + expect(symbols.definitions).toContainEqual( + expect.objectContaining({ name: 'utils', kind: 'namespace' }), + ); + }); + + it('extracts struct', () => { + const symbols = parseCpp(`struct Point { int x; int y; };`); + expect(symbols.definitions).toContainEqual( + expect.objectContaining({ name: 'Point', kind: 'struct' }), + ); + }); + + it('extracts enum', () => { + const symbols = parseCpp(`enum Color { RED, GREEN, BLUE };`); + expect(symbols.definitions).toContainEqual( + expect.objectContaining({ name: 'Color', kind: 'enum' }), + ); + }); + + it('extracts includes', () => { + const symbols = parseCpp(`#include \n#include "mylib.h"`); + expect(symbols.imports.length).toBe(2); + expect(symbols.imports[0].source).toBe('iostream'); + expect(symbols.imports[0].cInclude).toBe(true); + }); + + it('extracts function calls', () => { + const symbols = parseCpp(`void f() { std::cout << "hello"; bar(); }`); + expect(symbols.calls).toContainEqual(expect.objectContaining({ name: 'bar' })); + }); +}); diff --git a/tests/parsers/kotlin.test.ts b/tests/parsers/kotlin.test.ts new file mode 100644 index 00000000..3d08c315 --- /dev/null +++ b/tests/parsers/kotlin.test.ts @@ -0,0 +1,75 @@ +import { beforeAll, describe, expect, it } from 'vitest'; +import { createParsers, extractKotlinSymbols } from '../../src/domain/parser.js'; + +describe('Kotlin parser', () => { + let parsers: any; + + beforeAll(async () => { + parsers = await createParsers(); + }); + + function parseKotlin(code) { + const parser = parsers.get('kotlin'); + if (!parser) throw new Error('Kotlin parser not available'); + const tree = parser.parse(code); + return extractKotlinSymbols(tree, 'Test.kt'); + } + + it('extracts function declarations', () => { + const symbols = parseKotlin(`fun greet(name: String): String = "Hello"`); + expect(symbols.definitions).toContainEqual( + expect.objectContaining({ name: 'greet', kind: 'function' }), + ); + }); + + it('extracts class declarations', () => { + const symbols = parseKotlin(`class User { }`); + expect(symbols.definitions).toContainEqual( + expect.objectContaining({ name: 'User', kind: 'class', line: 1 }), + ); + }); + + it('extracts class with methods', () => { + const symbols = parseKotlin(`class User { + fun getName(): String = "Alice" +}`); + expect(symbols.definitions).toContainEqual( + expect.objectContaining({ name: 'User', kind: 'class' }), + ); + expect(symbols.definitions).toContainEqual( + expect.objectContaining({ name: 'User.getName', kind: 'method' }), + ); + }); + + it('extracts interface declarations', () => { + const symbols = parseKotlin(`interface Serializable { fun serialize(): String }`); + expect(symbols.definitions).toContainEqual( + expect.objectContaining({ name: 'Serializable', kind: 'interface', line: 1 }), + ); + }); + + it('extracts object declarations', () => { + const symbols = parseKotlin(`object Config { }`); + expect(symbols.definitions).toContainEqual( + expect.objectContaining({ name: 'Config', kind: 'class', line: 1 }), + ); + }); + + it('extracts inheritance', () => { + const symbols = parseKotlin(`open class Animal\nclass Dog : Animal() { }`); + expect(symbols.classes).toContainEqual( + expect.objectContaining({ name: 'Dog', extends: 'Animal' }), + ); + }); + + it('extracts imports', () => { + const symbols = parseKotlin(`import kotlin.collections.Map\nclass Foo { }`); + expect(symbols.imports).toContainEqual(expect.objectContaining({ kotlinImport: true })); + }); + + it('extracts function calls', () => { + const symbols = parseKotlin(`fun foo() { println("hello"); bar() }`); + expect(symbols.calls).toContainEqual(expect.objectContaining({ name: 'println' })); + expect(symbols.calls).toContainEqual(expect.objectContaining({ name: 'bar' })); + }); +}); diff --git a/tests/parsers/scala.test.ts b/tests/parsers/scala.test.ts new file mode 100644 index 00000000..a544768c --- /dev/null +++ b/tests/parsers/scala.test.ts @@ -0,0 +1,76 @@ +import { beforeAll, describe, expect, it } from 'vitest'; +import { createParsers, extractScalaSymbols } from '../../src/domain/parser.js'; + +describe('Scala parser', () => { + let parsers: any; + + beforeAll(async () => { + parsers = await createParsers(); + }); + + function parseScala(code) { + const parser = parsers.get('scala'); + if (!parser) throw new Error('Scala parser not available'); + const tree = parser.parse(code); + return extractScalaSymbols(tree, 'Test.scala'); + } + + it('extracts function definitions', () => { + const symbols = parseScala(`def greet(name: String): String = "Hello"`); + expect(symbols.definitions).toContainEqual( + expect.objectContaining({ name: 'greet', kind: 'function' }), + ); + }); + + it('extracts class definitions', () => { + const symbols = parseScala(`class User { }`); + expect(symbols.definitions).toContainEqual( + expect.objectContaining({ name: 'User', kind: 'class', line: 1 }), + ); + }); + + it('extracts class with methods', () => { + const symbols = parseScala(`class User { + def getName(): String = "Alice" +}`); + expect(symbols.definitions).toContainEqual( + expect.objectContaining({ name: 'User', kind: 'class' }), + ); + expect(symbols.definitions).toContainEqual( + expect.objectContaining({ name: 'User.getName', kind: 'method' }), + ); + }); + + it('extracts trait definitions', () => { + const symbols = parseScala(`trait Serializable { def serialize(): String }`); + expect(symbols.definitions).toContainEqual( + expect.objectContaining({ name: 'Serializable', kind: 'interface', line: 1 }), + ); + }); + + it('extracts object definitions', () => { + const symbols = parseScala(`object Config { }`); + expect(symbols.definitions).toContainEqual( + expect.objectContaining({ name: 'Config', kind: 'class', line: 1 }), + ); + }); + + it('extracts inheritance', () => { + const symbols = parseScala(`class Admin extends User { }`); + expect(symbols.classes).toContainEqual( + expect.objectContaining({ name: 'Admin', extends: 'User' }), + ); + }); + + it('extracts imports', () => { + const symbols = parseScala(`import scala.collection.mutable.Map +class Foo { }`); + expect(symbols.imports).toContainEqual(expect.objectContaining({ scalaImport: true })); + }); + + it('extracts function calls', () => { + const symbols = parseScala(`def foo(): Unit = { println("hello"); bar() }`); + expect(symbols.calls).toContainEqual(expect.objectContaining({ name: 'println' })); + expect(symbols.calls).toContainEqual(expect.objectContaining({ name: 'bar' })); + }); +}); diff --git a/tests/parsers/swift.test.ts b/tests/parsers/swift.test.ts new file mode 100644 index 00000000..5541bf20 --- /dev/null +++ b/tests/parsers/swift.test.ts @@ -0,0 +1,88 @@ +import { beforeAll, describe, expect, it } from 'vitest'; +import { createParsers, extractSwiftSymbols } from '../../src/domain/parser.js'; + +describe('Swift parser', () => { + let parsers: any; + + beforeAll(async () => { + parsers = await createParsers(); + }); + + function parseSwift(code) { + const parser = parsers.get('swift'); + if (!parser) throw new Error('Swift parser not available'); + const tree = parser.parse(code); + return extractSwiftSymbols(tree, 'Test.swift'); + } + + it('extracts function declarations', () => { + const symbols = parseSwift(`func greet(name: String) -> String { return "Hello" }`); + expect(symbols.definitions).toContainEqual( + expect.objectContaining({ name: 'greet', kind: 'function' }), + ); + }); + + it('extracts class declarations', () => { + const symbols = parseSwift(`class Animal { }`); + expect(symbols.definitions).toContainEqual( + expect.objectContaining({ name: 'Animal', kind: 'class', line: 1 }), + ); + }); + + it('extracts class with methods', () => { + const symbols = parseSwift(`class Animal { + func speak() { } +}`); + expect(symbols.definitions).toContainEqual( + expect.objectContaining({ name: 'Animal', kind: 'class' }), + ); + expect(symbols.definitions).toContainEqual( + expect.objectContaining({ name: 'Animal.speak', kind: 'method' }), + ); + }); + + it('extracts struct declarations', () => { + const symbols = parseSwift(`struct Point { + var x: Int + var y: Int +}`); + expect(symbols.definitions).toContainEqual( + expect.objectContaining({ name: 'Point', kind: 'struct', line: 1 }), + ); + }); + + it('extracts protocol declarations', () => { + const symbols = parseSwift(`protocol Drawable { func draw() }`); + expect(symbols.definitions).toContainEqual( + expect.objectContaining({ name: 'Drawable', kind: 'interface', line: 1 }), + ); + }); + + it('extracts enum declarations', () => { + const symbols = parseSwift(`enum Direction { + case north + case south +}`); + expect(symbols.definitions).toContainEqual( + expect.objectContaining({ name: 'Direction', kind: 'enum', line: 1 }), + ); + }); + + it('extracts inheritance', () => { + const symbols = parseSwift(`class Dog: Animal { }`); + expect(symbols.classes).toContainEqual( + expect.objectContaining({ name: 'Dog', extends: 'Animal' }), + ); + }); + + it('extracts imports', () => { + const symbols = parseSwift(`import Foundation`); + expect(symbols.imports).toContainEqual(expect.objectContaining({ swiftImport: true })); + }); + + it('extracts function calls', () => { + const symbols = parseSwift(`func foo() { print("hello"); bar() }`); + expect(symbols.calls).toContainEqual(expect.objectContaining({ name: 'print' })); + expect(symbols.calls).toContainEqual(expect.objectContaining({ name: 'bar' })); + }); +}); diff --git a/tests/search/embedding-regression.test.ts b/tests/search/embedding-regression.test.ts index a0391b7e..ea08c5b7 100644 --- a/tests/search/embedding-regression.test.ts +++ b/tests/search/embedding-regression.test.ts @@ -66,7 +66,7 @@ describe.skipIf(!hasTransformers)('embedding regression (real model)', () => { // Build embeddings with the smallest/fastest model await buildEmbeddings(tmpDir, 'minilm', dbPath); - }, 120_000); + }, 240_000); afterAll(() => { if (tmpDir) fs.rmSync(tmpDir, { recursive: true, force: true });