diff --git a/.gitignore b/.gitignore index d9ec29d..62bd592 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ node_modules *.vsix .idea/ */bin +target/ diff --git a/README.md b/README.md index b281e2a..d311093 100644 --- a/README.md +++ b/README.md @@ -102,7 +102,10 @@ The extension will automatically find coverage reports in `trident-tests` and vi ## Requirements - Visual Studio Code 1.96.0 or newer -- Rust and Cargo (latest stable) for Solana program security scanning +- Rust nightly toolchain (`nightly-2025-09-18`) for Solana program security scanning + - Install with: `rustup toolchain install nightly-2025-09-18` +- dylint-driver for running security detectors + - Install with: `cargo install cargo-dylint dylint-link` - Trident tests in your workspace for code coverage features ## Getting Started diff --git a/build_all_lints.sh b/build_all_lints.sh new file mode 100755 index 0000000..66518d9 --- /dev/null +++ b/build_all_lints.sh @@ -0,0 +1,103 @@ +#!/bin/bash + +# Build all Dylint lints and copy them to the extension directory +# This script should be run before packaging the extension + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "$SCRIPT_DIR" + +# Required nightly toolchain version +REQUIRED_NIGHTLY="nightly-2025-09-18" + +echo "🔨 Building all Dylint lints with $REQUIRED_NIGHTLY..." +echo "" + +# Check if required nightly is installed +if ! rustup toolchain list | grep -q "$REQUIRED_NIGHTLY"; then + echo "⚠️ Required Rust toolchain not found: $REQUIRED_NIGHTLY" + echo "📦 Installing $REQUIRED_NIGHTLY..." + rustup toolchain install "$REQUIRED_NIGHTLY" + echo "✅ Toolchain installed" + echo "" +fi + +# Detect platform +OS=$(uname -s) +ARCH=$(uname -m) + +if [ "$OS" = "Darwin" ]; then + if [ "$ARCH" = "arm64" ]; then + PLATFORM="macos-arm64" + LIB_EXT="dylib" + else + PLATFORM="macos-x64" + LIB_EXT="dylib" + fi +elif [ "$OS" = "Linux" ]; then + if [ "$ARCH" = "x86_64" ]; then + PLATFORM="linux-x64" + LIB_EXT="so" + elif [ "$ARCH" = "aarch64" ]; then + PLATFORM="linux-arm64" + LIB_EXT="so" + else + echo "❌ Unsupported architecture: $ARCH" + exit 1 + fi +else + echo "❌ Unsupported OS: $OS" + exit 1 +fi + +echo "📦 Platform: $PLATFORM" +echo "" + +# Create output directory +OUTPUT_DIR="$SCRIPT_DIR/lints_compiled/$PLATFORM" +mkdir -p "$OUTPUT_DIR" + +# Find all detector directories +DETECTOR_DIRS=$(find "$SCRIPT_DIR/extension/detectors" -mindepth 1 -maxdepth 1 -type d 2>/dev/null || true) + +if [ -z "$DETECTOR_DIRS" ]; then + echo "⚠️ No detector directories found in extension/detectors" + exit 1 +fi + +# Build each detector +for DETECTOR_DIR in $DETECTOR_DIRS; do + DETECTOR_NAME=$(basename "$DETECTOR_DIR") + echo "🔧 Building detector: $DETECTOR_NAME" + + cd "$DETECTOR_DIR" + + # Build in debug mode (faster for development) + # Use --release for production builds + # The rust-toolchain file in each detector directory will be used automatically + cargo build + + # Find the built library + LIB_FILE=$(find target/debug -name "lib${DETECTOR_NAME}@*.$LIB_EXT" -o -name "lib${DETECTOR_NAME}.$LIB_EXT" | head -n 1) + + if [ -z "$LIB_FILE" ]; then + echo "⚠️ Warning: Could not find library for $DETECTOR_NAME" + continue + fi + + # Copy to output directory + cp "$LIB_FILE" "$OUTPUT_DIR/" + echo "✅ Copied $(basename "$LIB_FILE") to $OUTPUT_DIR" + echo "" +done + +cd "$SCRIPT_DIR" + +echo "" +echo "🎉 All detectors built successfully!" +echo "" +echo "📁 Compiled detectors are in: $OUTPUT_DIR" +ls -lh "$OUTPUT_DIR" +echo "" +echo "💡 To use these detectors, make sure they are included in the extension package." diff --git a/extension/.vscodeignore b/extension/.vscodeignore index c68168f..5264fb3 100644 --- a/extension/.vscodeignore +++ b/extension/.vscodeignore @@ -51,3 +51,7 @@ Thumbs.db desktop.ini *.zip *.vsix + +# Dylint detectors - exclude build artifacts but include source +detectors/**/target/** +detectors/**/Cargo.lock diff --git a/extension/detectors/unchecked_math/.cargo/config.toml b/extension/detectors/unchecked_math/.cargo/config.toml new file mode 100644 index 0000000..226eca5 --- /dev/null +++ b/extension/detectors/unchecked_math/.cargo/config.toml @@ -0,0 +1,6 @@ +[target.'cfg(all())'] +rustflags = ["-C", "linker=dylint-link"] + +# For Rust versions 1.74.0 and onward, the following alternative can be used +# (see https://github.com/rust-lang/cargo/pull/12535): +# linker = "dylint-link" diff --git a/extension/detectors/unchecked_math/.gitignore b/extension/detectors/unchecked_math/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/extension/detectors/unchecked_math/.gitignore @@ -0,0 +1 @@ +/target diff --git a/extension/detectors/unchecked_math/Cargo.lock b/extension/detectors/unchecked_math/Cargo.lock new file mode 100644 index 0000000..f3a2d7f --- /dev/null +++ b/extension/detectors/unchecked_math/Cargo.lock @@ -0,0 +1,1525 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "anstream" +version = "0.6.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" + +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2" +dependencies = [ + "windows-sys 0.60.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.60.2", +] + +[[package]] +name = "anyhow" +version = "1.0.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[package]] +name = "bitflags" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" + +[[package]] +name = "camino" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "276a59bf2b2c967788139340c9f0c5b12d7fd6630315c15c217e559de85d2609" +dependencies = [ + "serde_core", +] + +[[package]] +name = "cargo-platform" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "122ec45a44b270afd1402f351b782c676b173e3c3fb28d86ff7ebfb4d86a4ee4" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo_metadata" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "981a6f317983eec002839b90fae7411a85621410ae591a9cab2ecf5cb5744873" +dependencies = [ + "camino", + "cargo-platform", + "semver", + "serde", + "serde_json", + "thiserror 2.0.17", +] + +[[package]] +name = "cc" +version = "1.2.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37521ac7aabe3d13122dc382493e20c9416f299d2ccd5b3a5340a2570cdeb0f3" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "clippy_utils" +version = "0.1.92" +source = "git+https://github.com/rust-lang/rust-clippy?rev=20ce69b9a63bcd2756cd906fe0964d1e901e042a#20ce69b9a63bcd2756cd906fe0964d1e901e042a" +dependencies = [ + "arrayvec", + "itertools", + "rustc_apfloat", + "serde", +] + +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + +[[package]] +name = "compiletest_rs" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f150fe9105fcd2a57cad53f0c079a24de65195903ef670990f5909f695eac04c" +dependencies = [ + "diff", + "filetime", + "getopts", + "lazy_static", + "libc", + "log", + "miow", + "regex", + "rustfix", + "serde", + "serde_derive", + "serde_json", + "tester", + "windows-sys 0.59.0", +] + +[[package]] +name = "diff" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" + +[[package]] +name = "dirs-next" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" +dependencies = [ + "cfg-if", + "dirs-sys-next", +] + +[[package]] +name = "dirs-sys-next" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "dylint" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa9a937bab540c0c8cdcbd650a572e6899ef2b6ffbc277d61bd2ae8d17c0edce" +dependencies = [ + "anstyle", + "anyhow", + "cargo_metadata", + "dylint_internal", + "log", + "once_cell", + "semver", + "serde", + "serde_json", + "tempfile", +] + +[[package]] +name = "dylint_internal" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e11f358a59510be7fa5c4f412729fabbe31a3587a342e4241a6a72020a2a0c5" +dependencies = [ + "anstyle", + "anyhow", + "bitflags", + "cargo_metadata", + "git2", + "home", + "if_chain", + "log", + "regex", + "rustversion", + "serde", + "tar", + "thiserror 2.0.17", + "toml", +] + +[[package]] +name = "dylint_linting" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4588b33aafbd472a6468ad1521d74094faa2bbdb53d534c2d24320300ea94135" +dependencies = [ + "cargo_metadata", + "dylint_internal", + "paste", + "rustversion", + "serde", + "thiserror 2.0.17", + "toml", +] + +[[package]] +name = "dylint_testing" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdc27f344ddb5488eb16b6e0f8aec889a30fb7e4d135060d336cfa60d1fd671c" +dependencies = [ + "anyhow", + "cargo_metadata", + "compiletest_rs", + "dylint", + "dylint_internal", + "env_logger", + "once_cell", + "regex", + "serde_json", + "tempfile", +] + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "env_filter" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bf3c259d255ca70051b30e2e95b5446cdb8949ac4cd22c0d7fd634d89f568e2" +dependencies = [ + "log", + "regex", +] + +[[package]] +name = "env_logger" +version = "0.11.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f" +dependencies = [ + "anstream", + "anstyle", + "env_filter", + "jiff", + "log", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "filetime" +version = "0.2.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc0505cd1b6fa6580283f6bdf70a73fcf4aba1184038c90902b92b3dd0df63ed" +dependencies = [ + "cfg-if", + "libc", + "libredox", + "windows-sys 0.60.2", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52051878f80a721bb68ebfbc930e07b65ba72f2da88968ea5c06fd6ca3d3a127" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "getopts" +version = "0.2.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe4fbac503b8d1f88e6676011885f34b7174f46e59956bba534ba83abded4df" +dependencies = [ + "unicode-width", +] + +[[package]] +name = "getrandom" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", +] + +[[package]] +name = "git2" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2deb07a133b1520dc1a5690e9bd08950108873d7ed5de38dcc74d3b5ebffa110" +dependencies = [ + "bitflags", + "libc", + "libgit2-sys", + "log", + "openssl-probe", + "openssl-sys", + "url", +] + +[[package]] +name = "hashbrown" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" + +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + +[[package]] +name = "home" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "icu_collections" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" + +[[package]] +name = "icu_properties" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e93fcd3157766c0c8da2f8cff6ce651a31f0810eaa1c51ec363ef790bbb5fb99" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02845b3647bb045f1100ecd6480ff52f34c35f82d9880e029d329c21d1054899" + +[[package]] +name = "icu_provider" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "if_chain" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd62e6b5e86ea8eeeb8db1de02880a6abc01a397b2ebb64b5d74ac255318f5cb" + +[[package]] +name = "indexmap" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6717a8d2a5a929a1a2eb43a12812498ed141a0bcfb7e8f7844fbdbe4303bba9f" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "jiff" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be1f93b8b1eb69c77f24bbb0afdf66f54b632ee39af40ca21c4365a1d7347e49" +dependencies = [ + "jiff-static", + "log", + "portable-atomic", + "portable-atomic-util", + "serde", +] + +[[package]] +name = "jiff-static" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03343451ff899767262ec32146f6d559dd759fdadf42ff0e227c7c48f72594b4" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom 0.3.4", + "libc", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.177" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" + +[[package]] +name = "libgit2-sys" +version = "0.18.2+1.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c42fe03df2bd3c53a3a9c7317ad91d80c81cd1fb0caec8d7cc4cd2bfa10c222" +dependencies = [ + "cc", + "libc", + "libssh2-sys", + "libz-sys", + "openssl-sys", + "pkg-config", +] + +[[package]] +name = "libredox" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb" +dependencies = [ + "bitflags", + "libc", + "redox_syscall", +] + +[[package]] +name = "libssh2-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "220e4f05ad4a218192533b300327f5150e809b54c4ec83b5a1d91833601811b9" +dependencies = [ + "cc", + "libc", + "libz-sys", + "openssl-sys", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "libz-sys" +version = "1.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b70e7a7df205e92a1a4cd9aaae7898dac0aa555503cc0a649494d0d60e7651d" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "linux-raw-sys" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" + +[[package]] +name = "litemap" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" + +[[package]] +name = "log" +version = "0.4.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" + +[[package]] +name = "memchr" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" + +[[package]] +name = "miow" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "536bfad37a309d62069485248eeaba1e8d9853aaf951caaeaed0585a95346f08" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "num_cpus" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "openssl-probe" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" + +[[package]] +name = "openssl-sys" +version = "0.9.110" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a9f0075ba3c21b09f8e8b2026584b1d18d49388648f2fbbf3c97ea8deced8e2" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "portable-atomic" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" + +[[package]] +name = "portable-atomic-util" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "potential_utf" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +dependencies = [ + "zerovec", +] + +[[package]] +name = "proc-macro2" +version = "1.0.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "redox_users" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +dependencies = [ + "getrandom 0.2.16", + "libredox", + "thiserror 1.0.69", +] + +[[package]] +name = "regex" +version = "1.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" + +[[package]] +name = "rustc_apfloat" +version = "0.2.3+llvm-462a31f5a5ab" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "486c2179b4796f65bfe2ee33679acf0927ac83ecf583ad6c91c3b4570911b9ad" +dependencies = [ + "bitflags", + "smallvec", +] + +[[package]] +name = "rustfix" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82fa69b198d894d84e23afde8e9ab2af4400b2cba20d6bf2b428a8b01c222c5a" +dependencies = [ + "serde", + "serde_json", + "thiserror 1.0.69", + "tracing", +] + +[[package]] +name = "rustix" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" +dependencies = [ + "serde", + "serde_core", +] + +[[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.145" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", + "serde_core", +] + +[[package]] +name = "serde_spanned" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e24345aa0fe688594e73770a5f6d1b216508b4f93484c0026d521acd30134392" +dependencies = [ + "serde_core", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "syn" +version = "2.0.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da58917d35242480a05c2897064da0a80589a2a0476c9a3f2fdc83b53502e917" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tar" +version = "0.4.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d863878d212c87a19c1a610eb53bb01fe12951c0501cf5a0d65f724914a667a" +dependencies = [ + "filetime", + "libc", + "xattr", +] + +[[package]] +name = "tempfile" +version = "3.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" +dependencies = [ + "fastrand", + "getrandom 0.3.4", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "term" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c59df8ac95d96ff9bede18eb7300b0fda5e5d8d90960e76f8e14ae765eedbf1f" +dependencies = [ + "dirs-next", + "rustversion", + "winapi", +] + +[[package]] +name = "tester" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89e8bf7e0eb2dd7b4228cc1b6821fc5114cd6841ae59f652a85488c016091e5f" +dependencies = [ + "cfg-if", + "getopts", + "libc", + "num_cpus", + "term", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" +dependencies = [ + "thiserror-impl 2.0.17", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tinystr" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "toml" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0dc8b1fb61449e27716ec0e1bdf0f6b8f3e8f6b05391e8497b8b6d7804ea6d8" +dependencies = [ + "indexmap", + "serde_core", + "serde_spanned", + "toml_datetime", + "toml_parser", + "toml_writer", + "winnow", +] + +[[package]] +name = "toml_datetime" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2cdb639ebbc97961c51720f858597f7f24c4fc295327923af55b74c3c724533" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_parser" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0cbe268d35bdb4bb5a56a2de88d0ad0eb70af5384a99d648cd4b3d04039800e" +dependencies = [ + "winnow", +] + +[[package]] +name = "toml_writer" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df8b2b54733674ad286d16267dcfc7a71ed5c776e4ac7aa3c3e2561f7c637bf2" + +[[package]] +name = "tracing" +version = "0.1.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +dependencies = [ + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" +dependencies = [ + "once_cell", +] + +[[package]] +name = "unchecked_math" +version = "0.1.0" +dependencies = [ + "clippy_utils", + "dylint_linting", + "dylint_testing", +] + +[[package]] +name = "unicode-ident" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" + +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + +[[package]] +name = "url" +version = "2.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.1+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "winnow" +version = "0.7.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" + +[[package]] +name = "wit-bindgen" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" + +[[package]] +name = "writeable" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" + +[[package]] +name = "xattr" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" +dependencies = [ + "libc", + "rustix", +] + +[[package]] +name = "yoke" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerotrie" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/extension/detectors/unchecked_math/Cargo.toml b/extension/detectors/unchecked_math/Cargo.toml new file mode 100644 index 0000000..5c3c1bf --- /dev/null +++ b/extension/detectors/unchecked_math/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "unchecked_math" +version = "0.1.0" +edition = "2024" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +clippy_utils = { git = "https://github.com/rust-lang/rust-clippy", rev = "20ce69b9a63bcd2756cd906fe0964d1e901e042a" } +dylint_linting = "5.0.0" + +[dev-dependencies] +dylint_testing = "5.0.0" + +[package.metadata.rust-analyzer] +rustc_private = true diff --git a/extension/detectors/unchecked_math/README.md b/extension/detectors/unchecked_math/README.md new file mode 100644 index 0000000..e983893 --- /dev/null +++ b/extension/detectors/unchecked_math/README.md @@ -0,0 +1,21 @@ +# template + +### What it does + +### Why is this bad? + +### Known problems + +Remove if none. + +### Example + +```rust +// example code where a warning is issued +``` + +Use instead: + +```rust +// example code that does not raise a warning +``` diff --git a/extension/detectors/unchecked_math/rust-toolchain b/extension/detectors/unchecked_math/rust-toolchain new file mode 100644 index 0000000..b8e5648 --- /dev/null +++ b/extension/detectors/unchecked_math/rust-toolchain @@ -0,0 +1,3 @@ +[toolchain] +channel = "nightly-2025-09-18" +components = ["llvm-tools-preview", "rustc-dev"] diff --git a/extension/detectors/unchecked_math/src/lib.rs b/extension/detectors/unchecked_math/src/lib.rs new file mode 100644 index 0000000..79d6b30 --- /dev/null +++ b/extension/detectors/unchecked_math/src/lib.rs @@ -0,0 +1,217 @@ +#![feature(rustc_private)] +#![warn(unused_extern_crates)] + +extern crate rustc_ast; +extern crate rustc_hir; +extern crate rustc_middle; + +use rustc_hir::{BinOpKind, Expr, ExprKind}; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_middle::ty::TyKind; + +dylint_linting::declare_late_lint! { + /// ### What it does + /// Detects unchecked arithmetic operations (addition, subtraction, multiplication, division) + /// that could lead to overflow or underflow vulnerabilities in Solana programs. + /// + /// ### Why is this bad? + /// In Solana programs, arithmetic overflow/underflow can lead to serious security vulnerabilities: + /// - Token amount manipulation + /// - Incorrect balance calculations + /// - Access control bypasses + /// - Economic exploits + /// + /// ### Example + /// + /// Bad: + /// ```rust + /// let total = amount1 + amount2; // Could overflow + /// let balance = balance - withdrawal; // Could underflow + /// ``` + /// + /// Good: + /// ```rust + /// let total = amount1.checked_add(amount2).ok_or(ErrorCode::Overflow)?; + /// let balance = balance.checked_sub(withdrawal).ok_or(ErrorCode::Underflow)?; + /// ``` + pub UNCHECKED_MATH, + Warn, + "detects unchecked arithmetic operations that could overflow/underflow" +} + +impl<'tcx> LateLintPass<'tcx> for UncheckedMath { + fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'tcx>) { + // Skip if inside a macro expansion to avoid false positives + if expr.span.from_expansion() { + return; + } + + match expr.kind { + // Check binary operations (+, -, *, /) + ExprKind::Binary(op, left, right) => { + if let Some((msg, help)) = check_arithmetic_op(cx, op.node, left, right, false) { + clippy_utils::diagnostics::span_lint_and_help( + cx, + UNCHECKED_MATH, + expr.span, + msg, + None, + help, + ); + } + } + // Check compound assignment operators (+=, -=, *=, /=) + ExprKind::AssignOp(op, left, right) => { + if let Some((msg, help)) = + check_arithmetic_op(cx, op.node.into(), left, right, true) + { + clippy_utils::diagnostics::span_lint_and_help( + cx, + UNCHECKED_MATH, + expr.span, + msg, + None, + help, + ); + } + } + _ => {} + } + } +} + +/// Check if an arithmetic operation is potentially unsafe and return appropriate message +fn check_arithmetic_op<'tcx>( + cx: &LateContext<'tcx>, + op: BinOpKind, + left: &'tcx Expr<'tcx>, + right: &'tcx Expr<'tcx>, + is_assignment: bool, +) -> Option<(&'static str, &'static str)> { + // Only check arithmetic operations + if !matches!( + op, + BinOpKind::Add | BinOpKind::Sub | BinOpKind::Mul | BinOpKind::Div + ) { + return None; + } + + // Check if the operation is on numeric types that could overflow + if !is_potentially_unsafe_operation(cx, left, right) { + return None; + } + + // Return appropriate message based on operation type + Some(get_lint_message(op, is_assignment)) +} + +/// Get the appropriate lint message for an operation +fn get_lint_message(op: BinOpKind, is_assignment: bool) -> (&'static str, &'static str) { + match (op, is_assignment) { + (BinOpKind::Add, false) => ( + "unchecked addition operation detected", + "consider using `checked_add()` to prevent overflow/underflow", + ), + (BinOpKind::Add, true) => ( + "unchecked addition assignment operation detected", + "consider using `checked_add()` and reassigning the result to prevent overflow/underflow", + ), + (BinOpKind::Sub, false) => ( + "unchecked subtraction operation detected", + "consider using `checked_sub()` to prevent overflow/underflow", + ), + (BinOpKind::Sub, true) => ( + "unchecked subtraction assignment operation detected", + "consider using `checked_sub()` and reassigning the result to prevent overflow/underflow", + ), + (BinOpKind::Mul, false) => ( + "unchecked multiplication operation detected", + "consider using `checked_mul()` to prevent overflow/underflow", + ), + (BinOpKind::Mul, true) => ( + "unchecked multiplication assignment operation detected", + "consider using `checked_mul()` and reassigning the result to prevent overflow/underflow", + ), + (BinOpKind::Div, false) => ( + "unchecked division operation detected", + "consider using `checked_div()` to prevent overflow/underflow", + ), + (BinOpKind::Div, true) => ( + "unchecked division assignment operation detected", + "consider using `checked_div()` and reassigning the result to prevent overflow/underflow", + ), + _ => unreachable!("Only arithmetic operations should reach here"), + } +} + +/// Check if an operation is potentially unsafe (could overflow/underflow) +fn is_potentially_unsafe_operation<'tcx>( + cx: &LateContext<'tcx>, + left: &'tcx Expr<'tcx>, + right: &'tcx Expr<'tcx>, +) -> bool { + use rustc_middle::ty::{IntTy, UintTy}; + + // Get the types of the operands + let left_ty = cx.typeck_results().expr_ty(left); + let right_ty = cx.typeck_results().expr_ty(right); + + // Check if at least one operand is an integer type that could overflow + // We check ALL integer types including u8, i8, u16, i16, etc. + // Even small types like u8 can overflow (e.g., 255 + 1 = 0 in wrapping mode) + let has_integer_type = matches!( + left_ty.kind(), + TyKind::Int(IntTy::I8 | IntTy::I16 | IntTy::I32 | IntTy::I64 | IntTy::I128 | IntTy::Isize) + | TyKind::Uint( + UintTy::U8 | UintTy::U16 | UintTy::U32 | UintTy::U64 | UintTy::U128 | UintTy::Usize + ) + ) || matches!( + right_ty.kind(), + TyKind::Int(IntTy::I8 | IntTy::I16 | IntTy::I32 | IntTy::I64 | IntTy::I128 | IntTy::Isize) + | TyKind::Uint( + UintTy::U8 | UintTy::U16 | UintTy::U32 | UintTy::U64 | UintTy::U128 | UintTy::Usize + ) + ); + + if !has_integer_type { + return false; + } + + // Skip if both operands are small compile-time constants + // These are unlikely to overflow and often used for array indexing, etc. + if is_small_literal(left) && is_small_literal(right) { + return false; + } + + true +} + +/// Check if an expression is a small literal that's unlikely to overflow +/// +/// This helps reduce false positives for common patterns like: +/// - Array indexing: `arr[i + 1]` +/// - Small offsets: `value + 10` +/// - Common constants: `x * 2`, `y - 1` +fn is_small_literal(expr: &Expr<'_>) -> bool { + const SMALL_LITERAL_THRESHOLD: u128 = 1 << 16; // 65536 + + match &expr.kind { + // Integer literals + ExprKind::Lit(lit) => match lit.node { + rustc_ast::LitKind::Int(val, _) => val.get() < SMALL_LITERAL_THRESHOLD, + // Floats don't overflow in the same way (they become infinity/NaN) + rustc_ast::LitKind::Float(_, _) => true, + _ => false, + }, + // Unary negation of small literals (e.g., -1, -100) + ExprKind::Unary(rustc_hir::UnOp::Neg, inner) => is_small_literal(inner), + // Type casts of small literals (e.g., 1 as u64) + ExprKind::Cast(inner, _) => is_small_literal(inner), + _ => false, + } +} + +#[test] +fn ui() { + dylint_testing::ui_test(env!("CARGO_PKG_NAME"), "ui"); +} diff --git a/extension/detectors/unchecked_math/ui/main.rs b/extension/detectors/unchecked_math/ui/main.rs new file mode 100644 index 0000000..fa5fea5 --- /dev/null +++ b/extension/detectors/unchecked_math/ui/main.rs @@ -0,0 +1,6 @@ +fn main() { + let x = 1 + 2; // Should trigger warning + let y = x + 3; // Should trigger warning + let z = x - y; // Should NOT trigger (subtraction) + let w = x * y; // Should NOT trigger (multiplication) +} diff --git a/extension/detectors/unchecked_math/ui/main.stderr b/extension/detectors/unchecked_math/ui/main.stderr new file mode 100644 index 0000000..adcc52f --- /dev/null +++ b/extension/detectors/unchecked_math/ui/main.stderr @@ -0,0 +1,15 @@ +warning: unchecked subtraction operation detected + --> $DIR/main.rs:2:13 + | +LL | let x = 1 + 2; // Should trigger warning + | ^^^^^ + | + = note: `#[warn(addition_detector)]` on by default + +warning: unchecked subtraction operation detected + --> $DIR/main.rs:3:13 + | +LL | let y = x + 3; // Should NOT trigger warning + | ^^^^^ + +warning: 2 warnings emitted diff --git a/extension/src/commands.ts b/extension/src/commands.ts index 9da6002..eff1489 100644 --- a/extension/src/commands.ts +++ b/extension/src/commands.ts @@ -2,7 +2,7 @@ import * as vscode from "vscode"; import { ExtensionFeatureManagers } from "./extensionFeatureManagers"; import { CLOSE_COVERAGE, SHOW_COVERAGE } from "./coverage/commands"; import { RELOAD_DETECTORS, SCAN_WORKSPACE, SHOW_SCAN_OUTPUT } from "./detectors/commands"; -import { INSTALL_NIGHTLY, SHOW_STATUS_DETAILS } from "./statusBar/commands"; +import { INSTALL_NIGHTLY, SHOW_STATUS_DETAILS, INSTALL_DYLINT_DRIVER } from "./statusBar/commands"; import { StatusBarState } from "./statusBar/statusBarManager"; function registerCommands( @@ -47,10 +47,12 @@ function registerCommands( vscode.commands.registerCommand(SHOW_STATUS_DETAILS, async () => { const state = extensionFeatureManagers.statusBarManager.getCurrentState(); const isNightly = extensionFeatureManagers.statusBarManager.isNightlyRustAvailable(); + const isDylintDriver = extensionFeatureManagers.statusBarManager.isDylintDriverInstalled(); let message = `Solana Extension Status\n\n`; message += `State: ${state}\n`; message += `Nightly Rust: ${isNightly ? 'Available' : 'Not available'}\n`; + message += `dylint-driver: ${isDylintDriver ? 'Available' : 'Not available'}\n`; if (state === 'error') { message += `\nThe language server encountered an error. Check the output channel for details.`; @@ -64,15 +66,16 @@ function registerCommands( // Add command to install nightly Rust context.subscriptions.push( vscode.commands.registerCommand(INSTALL_NIGHTLY, async () => { + const requiredToolchain = 'nightly-2025-09-18'; const action = await vscode.window.showInformationMessage( - 'Install nightly Rust toolchain? This will run: rustup toolchain install nightly', + `Install Rust ${requiredToolchain} toolchain? This will run: rustup toolchain install ${requiredToolchain}`, 'Install', 'Cancel' ); if (action === 'Install') { const terminal = vscode.window.createTerminal('Install Nightly Rust'); - terminal.sendText('rustup toolchain install nightly'); + terminal.sendText(`rustup toolchain install ${requiredToolchain}`); terminal.show(); // Recheck toolchain after a delay @@ -84,13 +87,48 @@ function registerCommands( currentState === StatusBarState.Warn ? StatusBarState.Chill : currentState, - 'Nightly Rust is now available' + `Rust ${requiredToolchain} is now available` ); } }, 5000); } }) ); + + // Add command to install dylint-driver + context.subscriptions.push( + vscode.commands.registerCommand(INSTALL_DYLINT_DRIVER, async () => { + const action = await vscode.window.showInformationMessage( + `Install dylint-driver? This will run: cargo install cargo-dylint dylint-link`, + 'Install', + 'Cancel' + ); + + if (action === 'Install') { + const terminal = vscode.window.createTerminal('Install dylint-driver'); + terminal.sendText(`cargo install cargo-dylint dylint-link`); + terminal.show(); + + vscode.window.showInformationMessage( + 'Installing dylint-driver... This may take a few minutes. After installation completes, the driver will be initialized automatically.' + ); + + // Recheck after a longer delay (dylint installation takes time) + setTimeout(async () => { + await extensionFeatureManagers.statusBarManager.recheckRustToolchain(); + if (extensionFeatureManagers.statusBarManager.isDylintDriverInstalled()) { + const currentState = extensionFeatureManagers.statusBarManager.getCurrentState(); + extensionFeatureManagers.statusBarManager.updateStatus( + currentState === StatusBarState.Warn + ? StatusBarState.Chill + : currentState, + 'dylint-driver is now available' + ); + } + }, 10000); + } + }) + ); } export default registerCommands; diff --git a/extension/src/detectors/detectorsManager.ts b/extension/src/detectors/detectorsManager.ts index 9023cff..aaca1c3 100644 --- a/extension/src/detectors/detectorsManager.ts +++ b/extension/src/detectors/detectorsManager.ts @@ -26,6 +26,11 @@ interface FileIssueInfo { is_test_file: boolean; } +interface DetectorStatus { + status: string; // "initializing", "building", "running", "complete", "idle" + message: string; +} + export class DetectorsManager { private client?: LanguageClient; private outputChannel: OutputChannel; @@ -160,7 +165,7 @@ export class DetectorsManager { // Otherwise the run options are used const serverOptions: ServerOptions = { - run: { command: serverPath, transport: TransportKind.stdio }, + run: { command: serverPath, transport: TransportKind.stdio, options: { env: { RUST_LOG: 'info' } } }, debug: { command: serverPath, transport: TransportKind.stdio, options: { env: { RUST_LOG: 'debug' } } } }; @@ -199,10 +204,11 @@ export class DetectorsManager { // Listen for scan complete notifications this.client.onNotification('solana/scanComplete', (scanSummary: ScanSummary) => { this.handleScanComplete(scanSummary); - // Update status bar after scan completes (unless it was an error) - if (this.statusBarUpdateCallback && this.client?.state === State.Running) { - this.statusBarUpdateCallback(StatusBarState.Chill, 'Scan completed'); - } + }); + + // Listen for detector status notifications + this.client.onNotification('solana/detectorStatus', (detectorStatus: DetectorStatus) => { + this.handleDetectorStatus(detectorStatus); }); } @@ -246,6 +252,30 @@ export class DetectorsManager { } } + private handleDetectorStatus(detectorStatus: DetectorStatus) { + console.log('Received detector status notification:', detectorStatus); + + // Update status bar based on detector status + if (this.statusBarUpdateCallback) { + switch (detectorStatus.status) { + case 'initializing': + case 'building': + case 'running': + this.statusBarUpdateCallback(StatusBarState.Running, detectorStatus.message); + break; + case 'complete': + case 'idle': + this.statusBarUpdateCallback(StatusBarState.Chill, detectorStatus.message); + break; + default: + this.outputChannel.appendLine(`Unknown detector status: ${detectorStatus.status}`); + } + } + + // Log to output channel + this.outputChannel.appendLine(`Detector Status: ${detectorStatus.message}`); + } + dispose() { this.client?.stop(); this.outputChannel.dispose(); diff --git a/extension/src/statusBar/commands.ts b/extension/src/statusBar/commands.ts index 8bc488d..0a2bbb2 100644 --- a/extension/src/statusBar/commands.ts +++ b/extension/src/statusBar/commands.ts @@ -6,3 +6,4 @@ // Status bar-related commands export const SHOW_STATUS_DETAILS = "solana.showStatusDetails"; export const INSTALL_NIGHTLY = "solana.installNightly"; +export const INSTALL_DYLINT_DRIVER = "solana.installDylintDriver"; diff --git a/extension/src/statusBar/statusBarManager.ts b/extension/src/statusBar/statusBarManager.ts index 1e7c07a..42579d4 100644 --- a/extension/src/statusBar/statusBarManager.ts +++ b/extension/src/statusBar/statusBarManager.ts @@ -18,6 +18,7 @@ export class StatusBarManager implements vscode.Disposable { private currentState: StatusBarState = StatusBarState.Chill; private rustToolchainChecked: boolean = false; private isNightlyAvailable: boolean = false; + private isDylintDriverAvailable: boolean = false; private extensionVersion: string = 'unknown'; constructor() { @@ -37,12 +38,14 @@ export class StatusBarManager implements vscode.Disposable { this.updateStatus(StatusBarState.Running, 'Initializing...'); this.statusBarItem.show(); - // Check Rust toolchain on initialization (async, will update status when done) + // Check Rust toolchain and dylint-driver on initialization (async, will update status when done) this.checkRustToolchain().then(() => { // After toolchain check, if still in initial state, update to chill or warn if (this.currentState === StatusBarState.Running) { if (!this.isNightlyAvailable) { - this.updateStatus(StatusBarState.Warn, 'Nightly Rust version not used. Click to install nightly.'); + this.updateStatus(StatusBarState.Warn, 'Rust nightly-2025-09-18 not installed.'); + } else if (!this.isDylintDriverAvailable) { + this.updateStatus(StatusBarState.Warn, 'dylint-driver not installed.'); } else { this.updateStatus(StatusBarState.Chill, 'Solana extension is ready'); } @@ -67,7 +70,10 @@ export class StatusBarManager implements vscode.Disposable { case StatusBarState.Chill: tooltip.appendMarkdown(message || 'Ready'); if (this.isNightlyAvailable) { - tooltip.appendMarkdown('\n\n✅ Nightly Rust toolchain available'); + tooltip.appendMarkdown('\n\n✅ Rust nightly-2025-09-18 available'); + } + if (this.isDylintDriverAvailable) { + tooltip.appendMarkdown('\n\n✅ dylint-driver available'); } tooltip.appendMarkdown('\n\n---\n\n'); tooltip.appendMarkdown('**Actions:**\n\n'); @@ -79,10 +85,26 @@ export class StatusBarManager implements vscode.Disposable { break; case StatusBarState.Warn: - tooltip.appendMarkdown('⚠️ ' + (message || 'Nightly Rust version not used')); + tooltip.appendMarkdown('⚠️ ' + (message || 'Setup required')); tooltip.appendMarkdown('\n\n---\n\n'); + if (!this.isNightlyAvailable) { + tooltip.appendMarkdown('❌ Rust nightly-2025-09-18 not installed\n\n'); + } else { + tooltip.appendMarkdown('✅ Rust nightly-2025-09-18 installed\n\n'); + } + if (!this.isDylintDriverAvailable) { + tooltip.appendMarkdown('❌ dylint-driver not installed\n\n'); + } else { + tooltip.appendMarkdown('✅ dylint-driver installed\n\n'); + } + tooltip.appendMarkdown('---\n\n'); tooltip.appendMarkdown('**Actions:**\n\n'); - tooltip.appendMarkdown(`⬇️ [Install Nightly Rust](command:solana.installNightly "Install nightly Rust toolchain")\n`); + if (!this.isNightlyAvailable) { + tooltip.appendMarkdown(`⬇️ [Install nightly-2025-09-18](command:solana.installNightly "Install Rust nightly-2025-09-18 toolchain")\n`); + } + if (!this.isDylintDriverAvailable) { + tooltip.appendMarkdown(`⬇️ [Install dylint-driver](command:solana.installDylintDriver "Install dylint-driver")\n`); + } break; case StatusBarState.Error: @@ -105,12 +127,12 @@ export class StatusBarManager implements vscode.Disposable { switch (state) { case StatusBarState.Chill: // Check nightly status when transitioning to chill - // If nightly is not available, show warning instead - if (!this.isNightlyAvailable && this.rustToolchainChecked) { + // If required toolchain or dylint-driver is not available, show warning instead + if ((!this.isNightlyAvailable || !this.isDylintDriverAvailable) && this.rustToolchainChecked) { this.statusBarItem.text = '$(warning) Solana'; this.statusBarItem.tooltip = this.createRichTooltip(StatusBarState.Warn, message); this.statusBarItem.color = new vscode.ThemeColor('statusBarItem.warningForeground'); - this.statusBarItem.command = 'solana.installNightly'; + this.statusBarItem.command = undefined; // Actions are in tooltip } else { this.statusBarItem.text = 'Solana'; this.statusBarItem.tooltip = this.createRichTooltip(StatusBarState.Chill, message); @@ -145,64 +167,41 @@ export class StatusBarManager implements vscode.Disposable { } /** - * Check if nightly Rust toolchain is available/configured + * Check if nightly Rust toolchain and dylint-driver are available/configured */ private async checkRustToolchain(): Promise { if (this.rustToolchainChecked) { return; } - try { - // First, check workspace toolchain files - const workspaceFolders = vscode.workspace.workspaceFolders; - if (workspaceFolders && workspaceFolders.length > 0) { - const workspaceRoot = workspaceFolders[0].uri.fsPath; - - // Check for rust-toolchain.toml - const rustToolchainToml = path.join(workspaceRoot, 'rust-toolchain.toml'); - if (fs.existsSync(rustToolchainToml)) { - const content = fs.readFileSync(rustToolchainToml, 'utf-8'); - if (content.includes('channel = "nightly"') || content.includes('channel="nightly"')) { - this.isNightlyAvailable = true; - this.rustToolchainChecked = true; - return; - } - } - - // Check for rust-toolchain file (without .toml extension) - const rustToolchain = path.join(workspaceRoot, 'rust-toolchain'); - if (fs.existsSync(rustToolchain)) { - const content = fs.readFileSync(rustToolchain, 'utf-8'); - if (content.includes('nightly')) { - this.isNightlyAvailable = true; - this.rustToolchainChecked = true; - return; - } - } - } + const REQUIRED_TOOLCHAIN = 'nightly-2025-09-18'; - // Fallback: check if nightly is installed (regardless of whether it's active) + try { + // Check if the specific required toolchain is installed try { const { stdout } = await execAsync('rustup toolchain list'); - // Check if any nightly toolchain is installed - // Format: "nightly-YYYY-MM-DD-x86_64-apple-darwin (default)" or "nightly-YYYY-MM-DD-x86_64-apple-darwin" - if (stdout.includes('nightly')) { + // Check if the specific nightly toolchain is installed + // Format: "nightly-2025-09-18-x86_64-apple-darwin (default)" or "nightly-2025-09-18-x86_64-apple-darwin" + if (stdout.includes(REQUIRED_TOOLCHAIN)) { this.isNightlyAvailable = true; - this.rustToolchainChecked = true; - return; } } catch { // rustup might not be available } - // No nightly found - this.isNightlyAvailable = false; + // Check if dylint-driver is installed + await this.checkDylintDriver(); + this.rustToolchainChecked = true; - // Update status if we're in chill state and nightly is not available + // Update status if we're in chill state and something is not available // Only update to warn if we're currently in chill state (not running or error) if (this.currentState === StatusBarState.Chill) { - this.updateStatus(StatusBarState.Warn, 'Nightly Rust version not used. Click to install nightly.'); + if (!this.isNightlyAvailable) { + this.updateStatus(StatusBarState.Warn, `Rust ${REQUIRED_TOOLCHAIN} not installed.`); + } else if (!this.isDylintDriverAvailable) { + this.updateStatus(StatusBarState.Warn, 'dylint-driver not installed.'); + } } } catch (error) { console.error('Error checking Rust toolchain:', error); @@ -210,6 +209,35 @@ export class StatusBarManager implements vscode.Disposable { } } + /** + * Check if dylint-driver is installed + */ + private async checkDylintDriver(): Promise { + const REQUIRED_TOOLCHAIN = 'nightly-2025-09-18'; + try { + // Get home directory + const homeDir = process.env.HOME || process.env.USERPROFILE; + if (!homeDir) { + this.isDylintDriverAvailable = false; + return; + } + + // Determine platform-specific path + const arch = process.arch === 'x64' ? 'x86_64' : process.arch === 'arm64' ? 'aarch64' : process.arch; + const os = process.platform === 'darwin' ? 'apple-darwin' : + process.platform === 'linux' ? 'unknown-linux-gnu' : 'unknown'; + + const toolchainTarget = `${REQUIRED_TOOLCHAIN}-${arch}-${os}`; + const dylintDriverPath = path.join(homeDir, '.dylint_drivers', toolchainTarget, 'dylint-driver'); + + // Check if dylint-driver exists + this.isDylintDriverAvailable = fs.existsSync(dylintDriverPath); + } catch (error) { + console.error('Error checking dylint-driver:', error); + this.isDylintDriverAvailable = false; + } + } + /** * Get current state */ @@ -224,6 +252,13 @@ export class StatusBarManager implements vscode.Disposable { return this.isNightlyAvailable; } + /** + * Check if dylint-driver is available + */ + isDylintDriverInstalled(): boolean { + return this.isDylintDriverAvailable; + } + /** * Re-check Rust toolchain (useful after installing nightly) */ @@ -232,10 +267,14 @@ export class StatusBarManager implements vscode.Disposable { await this.checkRustToolchain(); // Update status based on new toolchain check - if (this.currentState === StatusBarState.Warn && this.isNightlyAvailable) { - this.updateStatus(StatusBarState.Chill, 'Nightly Rust is now available'); - } else if (this.currentState === StatusBarState.Chill && !this.isNightlyAvailable) { - this.updateStatus(StatusBarState.Warn, 'Nightly Rust version not used. Click to install nightly.'); + if (this.currentState === StatusBarState.Warn && this.isNightlyAvailable && this.isDylintDriverAvailable) { + this.updateStatus(StatusBarState.Chill, 'All requirements installed'); + } else if (this.currentState === StatusBarState.Chill && (!this.isNightlyAvailable || !this.isDylintDriverAvailable)) { + if (!this.isNightlyAvailable) { + this.updateStatus(StatusBarState.Warn, 'Rust nightly-2025-09-18 not installed.'); + } else if (!this.isDylintDriverAvailable) { + this.updateStatus(StatusBarState.Warn, 'dylint-driver not installed.'); + } } } diff --git a/language-server/Cargo.lock b/language-server/Cargo.lock index 8a4cd92..6f0e1b4 100644 --- a/language-server/Cargo.lock +++ b/language-server/Cargo.lock @@ -76,6 +76,12 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "anyhow" +version = "1.0.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" + [[package]] name = "async-stream" version = "0.3.6" @@ -138,7 +144,7 @@ dependencies = [ "miniz_oxide", "object", "rustc-demangle", - "windows-targets", + "windows-targets 0.52.6", ] [[package]] @@ -184,6 +190,27 @@ dependencies = [ "parking_lot_core", ] +[[package]] +name = "dirs" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.48.0", +] + [[package]] name = "displaydoc" version = "0.2.5" @@ -304,6 +331,17 @@ dependencies = [ "slab", ] +[[package]] +name = "getrandom" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + [[package]] name = "gimli" version = "0.31.1" @@ -469,7 +507,10 @@ dependencies = [ name = "language-server" version = "0.1.2" dependencies = [ + "anyhow", + "dirs", "env_logger", + "libloading", "log", "proc-macro2", "serde", @@ -486,6 +527,26 @@ version = "0.2.172" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link", +] + +[[package]] +name = "libredox" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb" +dependencies = [ + "bitflags 2.9.0", + "libc", +] + [[package]] name = "litemap" version = "0.8.0" @@ -562,6 +623,12 @@ version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + [[package]] name = "parking_lot" version = "0.12.3" @@ -582,7 +649,7 @@ dependencies = [ "libc", "redox_syscall", "smallvec", - "windows-targets", + "windows-targets 0.52.6", ] [[package]] @@ -674,6 +741,17 @@ dependencies = [ "bitflags 2.9.0", ] +[[package]] +name = "redox_users" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +dependencies = [ + "getrandom", + "libredox", + "thiserror", +] + [[package]] name = "regex" version = "1.11.1" @@ -826,6 +904,26 @@ dependencies = [ "syn", ] +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "tinystr" version = "0.8.1" @@ -1029,13 +1127,28 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + [[package]] name = "windows-sys" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets", + "windows-targets 0.52.6", ] [[package]] @@ -1044,7 +1157,22 @@ version = "0.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" dependencies = [ - "windows-targets", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", ] [[package]] @@ -1053,28 +1181,46 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", ] +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + [[package]] name = "windows_aarch64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + [[package]] name = "windows_i686_gnu" version = "0.52.6" @@ -1087,24 +1233,48 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + [[package]] name = "windows_i686_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + [[package]] name = "windows_x86_64_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + [[package]] name = "windows_x86_64_msvc" version = "0.52.6" diff --git a/language-server/Cargo.toml b/language-server/Cargo.toml index 0fef2fb..1df1a7a 100644 --- a/language-server/Cargo.toml +++ b/language-server/Cargo.toml @@ -17,6 +17,9 @@ syn = { version = "2.0.101", features = [ "parsing", ] } proc-macro2 = { version = "1.0.95", features = ["span-locations"] } +anyhow = "1.0" +dirs = "5.0" +libloading = "0.8" [dev-dependencies] tokio-test = "0.4" diff --git a/language-server/README.md b/language-server/README.md index 19306a3..8c18949 100644 --- a/language-server/README.md +++ b/language-server/README.md @@ -24,8 +24,13 @@ This language server provides automated security analysis for Solana smart contr ### Prerequisites -- Rust toolchain (latest stable) +- Rust nightly toolchain (`nightly-2025-09-18`) + - Install with: `rustup toolchain install nightly-2025-09-18` + - Required components: `llvm-tools-preview`, `rustc-dev` - Cargo package manager +- dylint-driver for running security detectors + - Install with: `cargo install cargo-dylint dylint-link` + - Initialize with: `cargo +nightly-2025-09-18 dylint --list` ### Building diff --git a/language-server/src/backend.rs b/language-server/src/backend.rs index fd8891b..d3dd9a6 100644 --- a/language-server/src/backend.rs +++ b/language-server/src/backend.rs @@ -1,11 +1,15 @@ +use crate::core::dylint::constants::REQUIRED_NIGHTLY_VERSION; use crate::core::{ - DetectorInfo, DetectorRegistry, DetectorRegistryBuilder, FileScanner, + DetectorInfo, DetectorRegistry, DetectorRegistryBuilder, DetectorStatus, + DetectorStatusNotification, DylintDetectorManager, FileScanner, ImmutableAccountMutatedDetector, InstructionAttributeInvalidDetector, InstructionAttributeUnusedDetector, ManualLamportsZeroingDetector, MissingCheckCommentDetector, MissingInitspaceDetector, MissingSignerDetector, ScanCompleteNotification, ScanResult, - ScanSummary, SysvarAccountDetector, UnsafeMathDetector, + ScanSummary, SysvarAccountDetector, }; -use log::info; +use crate::dylint_runner::DylintRunner; +use log::{info, warn}; +use std::path::PathBuf; use std::sync::Arc; use tokio::sync::Mutex; use tower_lsp::{ @@ -24,6 +28,9 @@ pub struct Backend { client: Client, detector_registry: Arc>, file_scanner: Arc>, + dylint_runner: Option>, + dylint_manager: Arc>>, + workspace_root: Arc>>, } #[tower_lsp::async_trait] @@ -33,106 +40,320 @@ impl LanguageServer for Backend { params: InitializeParams, ) -> Result { // Set up workspace root if provided - if let Some(workspace_folders) = params.workspace_folders { - if let Some(folder) = workspace_folders.first() { - if let Ok(path) = folder.uri.to_file_path() { - let mut scanner = self.file_scanner.lock().await; - scanner.set_workspace_root(path); - - // Perform initial workspace scan - info!("Performing initial workspace scan..."); - let mut registry = self.detector_registry.lock().await; - let scan_result = scanner.scan_workspace(&mut registry).await; + if let Some(workspace_folders) = params.workspace_folders + && let Some(folder) = workspace_folders.first() + && let Ok(path) = folder.uri.to_file_path() + { + // Store workspace root for dylint + *self.workspace_root.lock().await = Some(path.clone()); - // Log scan results - info!("Initial scan completed:"); - info!(" - {} Rust files found", scan_result.rust_files.len()); - info!( - " - {} Anchor programs found", - scan_result.anchor_program_files().len() - ); - info!( - " - {} files with security issues", - scan_result.files_with_issues().len() - ); - info!( - " - {} total security issues found", - scan_result.total_issues() - ); - info!( - " - {} Anchor.toml files found", - scan_result.anchor_configs.len() - ); - info!( - " - {} Cargo.toml files found", - scan_result.cargo_files.len() - ); + let mut scanner = self.file_scanner.lock().await; + scanner.set_workspace_root(path.clone()); - // Publish diagnostics for ALL scanned files (including empty diagnostics for fixed files) - for file_info in &scan_result.rust_files { - if let Ok(uri) = tower_lsp::lsp_types::Url::from_file_path(&file_info.path) - { - self.client - .publish_diagnostics(uri, file_info.diagnostics.clone(), None) + // Perform initial workspace scan + info!("Performing initial workspace scan..."); + let mut registry = self.detector_registry.lock().await; + let scan_result = scanner.scan_workspace(&mut registry).await; + drop(registry); // Release registry lock before initializing dylint + drop(scanner); // Release scanner lock + + // Log scan results + info!("Initial scan completed:"); + info!(" - {} Rust files found", scan_result.rust_files.len()); + info!( + " - {} Anchor programs found", + scan_result.anchor_program_files().len() + ); + info!( + " - {} files with security issues", + scan_result.files_with_issues().len() + ); + info!( + " - {} total security issues found", + scan_result.total_issues() + ); + info!( + " - {} Anchor.toml files found", + scan_result.anchor_configs.len() + ); + info!( + " - {} Cargo.toml files found", + scan_result.cargo_files.len() + ); + + // Publish diagnostics for ALL scanned files (including empty diagnostics for fixed files) + for file_info in &scan_result.rust_files { + if let Ok(uri) = tower_lsp::lsp_types::Url::from_file_path(&file_info.path) { + self.client + .publish_diagnostics(uri, file_info.diagnostics.clone(), None) + .await; + } + } + + // Send scan results to extension (initial scan, not manual) + let scan_summary = ScanSummary::from_scan_result(&scan_result, false); + self.client + .send_notification::(scan_summary) + .await; + + // Initialize dylint detectors on project open + info!("[Extension Dylint] Initializing detectors on project open..."); + + // Notify extension that detectors are initializing + self.client + .send_notification::(DetectorStatus { + status: "initializing".to_string(), + message: "Initializing security detectors...".to_string(), + }) + .await; + + Self::ensure_dylint_detectors_initialized(self).await; + + // Run dylint in background and merge with syn diagnostics + if let Some(dylint_runner) = &self.dylint_runner { + let runner = Arc::clone(dylint_runner); + let workspace = path.clone(); + let client = self.client.clone(); + let file_list: Vec<(std::path::PathBuf, Vec)> = + scan_result + .rust_files + .iter() + .map(|f| (f.path.clone(), f.diagnostics.clone())) + .collect(); + + tokio::spawn(async move { + info!("Running dylint on project open..."); + + // Notify that detectors are running + client + .send_notification::(DetectorStatus { + status: "running".to_string(), + message: "Running security detectors...".to_string(), + }) + .await; + + match runner.run_lints(&workspace).await { + Ok(dylint_diagnostics) => { + info!( + "Dylint found {} total issues on project open", + dylint_diagnostics.len() + ); + + // Merge dylint diagnostics with syn diagnostics for each file + for (file_path, syn_diagnostics) in file_list { + if let Ok(uri) = + tower_lsp::lsp_types::Url::from_file_path(&file_path) + { + // Filter dylint diagnostics for this file + let dylint_file_diagnostics: Vec<_> = dylint_diagnostics + .iter() + .filter(|d| { + let path_str = file_path.to_string_lossy(); + path_str.ends_with(&d.file_name) + || path_str.contains(&d.file_name) + }) + .map(|d| d.to_lsp_diagnostic()) + .collect(); + + if !dylint_file_diagnostics.is_empty() { + // Merge syn and dylint diagnostics + let mut merged_diagnostics = syn_diagnostics.clone(); + merged_diagnostics.extend(dylint_file_diagnostics.clone()); + + info!( + "Publishing {} total diagnostics ({} syn + {} dylint) for {}", + merged_diagnostics.len(), + syn_diagnostics.len(), + dylint_file_diagnostics.len(), + file_path.display() + ); + + // Publish merged diagnostics + client + .publish_diagnostics(uri, merged_diagnostics, None) + .await; + } + } + } + + // Notify complete + client + .send_notification::(DetectorStatus { + status: "complete".to_string(), + message: "Security scan complete".to_string(), + }) + .await; + } + Err(e) => { + info!("Dylint failed on project open: {}", e); + + // Notify complete even on error + client + .send_notification::(DetectorStatus { + status: "complete".to_string(), + message: "Security scan complete".to_string(), + }) .await; } } + }); + } + } else if let Some(root_uri) = params.root_uri + && let Ok(path) = root_uri.to_file_path() + { + // Store workspace root for dylint + *self.workspace_root.lock().await = Some(path.clone()); - // Send scan results to extension (initial scan, not manual) - let scan_summary = ScanSummary::from_scan_result(&scan_result, false); + let mut scanner = self.file_scanner.lock().await; + scanner.set_workspace_root(path.clone()); + + // Perform initial workspace scan + info!("Performing initial workspace scan..."); + let mut registry = self.detector_registry.lock().await; + let scan_result = scanner.scan_workspace(&mut registry).await; + drop(registry); // Release registry lock before initializing dylint + drop(scanner); // Release scanner lock + + // Log scan results + info!("Initial scan completed:"); + info!(" - {} Rust files found", scan_result.rust_files.len()); + info!( + " - {} Anchor programs found", + scan_result.anchor_program_files().len() + ); + info!( + " - {} files with security issues", + scan_result.files_with_issues().len() + ); + info!( + " - {} total security issues found", + scan_result.total_issues() + ); + info!( + " - {} Anchor.toml files found", + scan_result.anchor_configs.len() + ); + info!( + " - {} Cargo.toml files found", + scan_result.cargo_files.len() + ); + + // Publish diagnostics for ALL scanned files (including empty diagnostics for fixed files) + for file_info in &scan_result.rust_files { + if let Ok(uri) = tower_lsp::lsp_types::Url::from_file_path(&file_info.path) { self.client - .send_notification::(scan_summary) + .publish_diagnostics(uri, file_info.diagnostics.clone(), None) .await; } } - } else if let Some(root_uri) = params.root_uri { - if let Ok(path) = root_uri.to_file_path() { - let mut scanner = self.file_scanner.lock().await; - scanner.set_workspace_root(path); - - // Perform initial workspace scan - info!("Performing initial workspace scan..."); - let mut registry = self.detector_registry.lock().await; - let scan_result = scanner.scan_workspace(&mut registry).await; - - // Log scan results - info!("Initial scan completed:"); - info!(" - {} Rust files found", scan_result.rust_files.len()); - info!( - " - {} Anchor programs found", - scan_result.anchor_program_files().len() - ); - info!( - " - {} files with security issues", - scan_result.files_with_issues().len() - ); - info!( - " - {} total security issues found", - scan_result.total_issues() - ); - info!( - " - {} Anchor.toml files found", - scan_result.anchor_configs.len() - ); - info!( - " - {} Cargo.toml files found", - scan_result.cargo_files.len() - ); - // Publish diagnostics for ALL scanned files (including empty diagnostics for fixed files) - for file_info in &scan_result.rust_files { - if let Ok(uri) = tower_lsp::lsp_types::Url::from_file_path(&file_info.path) { - self.client - .publish_diagnostics(uri, file_info.diagnostics.clone(), None) - .await; - } - } + // Send scan results to extension (initial scan, not manual) + let scan_summary = ScanSummary::from_scan_result(&scan_result, false); + self.client + .send_notification::(scan_summary) + .await; + + // Initialize dylint detectors on project open + info!("[Extension Dylint] Initializing detectors on project open..."); + + // Notify extension that detectors are initializing + self.client + .send_notification::(DetectorStatus { + status: "initializing".to_string(), + message: "Initializing security detectors...".to_string(), + }) + .await; + + Self::ensure_dylint_detectors_initialized(self).await; + + // Run dylint in background and merge with syn diagnostics + if let Some(dylint_runner) = &self.dylint_runner { + let runner = Arc::clone(dylint_runner); + let workspace = path.clone(); + let client = self.client.clone(); + let file_list: Vec<(std::path::PathBuf, Vec)> = + scan_result + .rust_files + .iter() + .map(|f| (f.path.clone(), f.diagnostics.clone())) + .collect(); + + tokio::spawn(async move { + info!("Running dylint on project open..."); + + // Notify that detectors are running + client + .send_notification::(DetectorStatus { + status: "running".to_string(), + message: "Running security detectors...".to_string(), + }) + .await; - // Send scan results to extension (initial scan, not manual) - let scan_summary = ScanSummary::from_scan_result(&scan_result, false); - self.client - .send_notification::(scan_summary) - .await; + match runner.run_lints(&workspace).await { + Ok(dylint_diagnostics) => { + info!( + "Dylint found {} total issues on project open", + dylint_diagnostics.len() + ); + + // Merge dylint diagnostics with syn diagnostics for each file + for (file_path, syn_diagnostics) in file_list { + if let Ok(uri) = + tower_lsp::lsp_types::Url::from_file_path(&file_path) + { + // Filter dylint diagnostics for this file + let dylint_file_diagnostics: Vec<_> = dylint_diagnostics + .iter() + .filter(|d| { + let path_str = file_path.to_string_lossy(); + path_str.ends_with(&d.file_name) + || path_str.contains(&d.file_name) + }) + .map(|d| d.to_lsp_diagnostic()) + .collect(); + + if !dylint_file_diagnostics.is_empty() { + // Merge syn and dylint diagnostics + let mut merged_diagnostics = syn_diagnostics.clone(); + merged_diagnostics.extend(dylint_file_diagnostics.clone()); + + info!( + "Publishing {} total diagnostics ({} syn + {} dylint) for {}", + merged_diagnostics.len(), + syn_diagnostics.len(), + dylint_file_diagnostics.len(), + file_path.display() + ); + + // Publish merged diagnostics + client + .publish_diagnostics(uri, merged_diagnostics, None) + .await; + } + } + } + + // Notify complete + client + .send_notification::(DetectorStatus { + status: "complete".to_string(), + message: "Security scan complete".to_string(), + }) + .await; + } + Err(e) => { + info!("Dylint failed on project open: {}", e); + + // Notify complete even on error + client + .send_notification::(DetectorStatus { + status: "complete".to_string(), + message: "Security scan complete".to_string(), + }) + .await; + } + } + }); } } @@ -177,6 +398,7 @@ impl LanguageServer for Backend { async fn did_save(&self, _params: DidSaveTextDocumentParams) { info!("File saved, reloading detectors and performing full workspace scan..."); + info!("[DEBUG] About to initialize dylint detectors..."); // Create a new detector registry with fresh detector instances let new_registry = create_default_registry(); @@ -194,7 +416,7 @@ impl LanguageServer for Backend { scanner.scan_workspace(&mut registry).await }; - // Publish diagnostics for ALL scanned files (including empty diagnostics for fixed files) + // Publish syn diagnostics for ALL scanned files for file_info in &scan_result.rust_files { if let Ok(uri) = tower_lsp::lsp_types::Url::from_file_path(&file_info.path) { self.client @@ -208,6 +430,103 @@ impl LanguageServer for Backend { self.client .send_notification::(scan_summary) .await; + + // Initialize dylint detectors on first save (lazy initialization) + // This checks if nightly is available and compiles/caches detectors + + // Notify extension that detectors are running + self.client + .send_notification::(DetectorStatus { + status: "running".to_string(), + message: "Running security detectors...".to_string(), + }) + .await; + + Self::ensure_dylint_detectors_initialized(self).await; + + // Run dylint in background and merge with syn diagnostics + if let Some(dylint_runner) = &self.dylint_runner { + if let Some(workspace_root) = self.workspace_root.lock().await.as_ref() { + let runner = Arc::clone(dylint_runner); + let workspace = workspace_root.clone(); + let client = self.client.clone(); + // Create a simplified file list for dylint merging + let file_list: Vec<(std::path::PathBuf, Vec)> = + scan_result + .rust_files + .iter() + .map(|f| (f.path.clone(), f.diagnostics.clone())) + .collect(); + + tokio::spawn(async move { + info!("Running dylint after save..."); + match runner.run_lints(&workspace).await { + Ok(dylint_diagnostics) => { + info!( + "Dylint found {} total issues after save", + dylint_diagnostics.len() + ); + + // Merge dylint diagnostics with syn diagnostics for each file + for (file_path, syn_diagnostics) in file_list { + if let Ok(uri) = + tower_lsp::lsp_types::Url::from_file_path(&file_path) + { + // Filter dylint diagnostics for this file + let dylint_file_diagnostics: Vec<_> = dylint_diagnostics + .iter() + .filter(|d| { + let path_str = file_path.to_string_lossy(); + path_str.ends_with(&d.file_name) + || path_str.contains(&d.file_name) + }) + .map(|d| d.to_lsp_diagnostic()) + .collect(); + + if !dylint_file_diagnostics.is_empty() { + // Merge syn and dylint diagnostics + let mut merged_diagnostics = syn_diagnostics.clone(); + merged_diagnostics.extend(dylint_file_diagnostics.clone()); + + info!( + "Publishing {} total diagnostics ({} syn + {} dylint) for {}", + merged_diagnostics.len(), + syn_diagnostics.len(), + dylint_file_diagnostics.len(), + file_path.display() + ); + + // Publish merged diagnostics + client + .publish_diagnostics(uri, merged_diagnostics, None) + .await; + } + } + } + + // Notify complete + client + .send_notification::(DetectorStatus { + status: "complete".to_string(), + message: "Security scan complete".to_string(), + }) + .await; + } + Err(e) => { + info!("Dylint failed after save: {}", e); + + // Notify complete even on error + client + .send_notification::(DetectorStatus { + status: "complete".to_string(), + message: "Security scan complete".to_string(), + }) + .await; + } + } + }); + } + } } async fn execute_command( @@ -290,25 +609,256 @@ impl LanguageServer for Backend { impl Backend { pub fn new(client: Client) -> Backend { + // Try to initialize dylint runner (for pre-compiled detectors) + let dylint_runner = Self::try_init_dylint_runner(); + Backend { client, detector_registry: Arc::new(Mutex::new(create_default_registry())), file_scanner: Arc::new(Mutex::new(FileScanner::default())), + dylint_runner, + dylint_manager: Arc::new(Mutex::new(None)), + workspace_root: Arc::new(Mutex::new(None)), + } + } + + /// Ensure dylint detectors are initialized (lazy initialization on first save) + /// This checks if detectors have been initialized, and if not: + /// 1. Checks if nightly Rust is available + /// 2. Scans for detector source code in extension/detectors/ + /// 3. Checks cache for compiled versions matching current nightly + /// 4. Compiles and caches if not present + /// 5. Adds compiled detectors to dylint runner + async fn ensure_dylint_detectors_initialized(&self) { + info!("[DEBUG] ensure_dylint_detectors_initialized called"); + + // Check if already initialized + { + let manager_lock = self.dylint_manager.lock().await; + info!("[DEBUG] Got manager lock, checking initialization status..."); + if let Some(manager) = manager_lock.as_ref() { + info!("[DEBUG] Manager exists, checking if initialized"); + if manager.is_initialized() { + // Already initialized, nothing to do + info!("[DEBUG] Manager already initialized, skipping"); + return; + } + info!("[DEBUG] Manager not initialized yet"); + } else { + info!("[DEBUG] Manager is None, will create new one"); + } + } + + info!("[Extension Dylint] Initializing detectors on first save..."); + + // Check if the required nightly version is available + if !DylintDetectorManager::check_nightly_available() { + warn!( + "[Extension Dylint] Required nightly Rust version not available: {}", + REQUIRED_NIGHTLY_VERSION + ); + warn!( + "[Extension Dylint] Install with: rustup toolchain install {}", + REQUIRED_NIGHTLY_VERSION + ); + return; + } + + // Check if dylint-driver is available + if !DylintDetectorManager::check_dylint_driver_available() { + warn!("[Extension Dylint] dylint-driver not found"); + warn!("[Extension Dylint] Install with: cargo install cargo-dylint dylint-link"); + warn!( + "[Extension Dylint] Then initialize: cargo +{} dylint --list", + REQUIRED_NIGHTLY_VERSION + ); + return; + } + + // Get extension path (where detectors are bundled) + let extension_path = match std::env::current_exe() { + Ok(exe_path) => exe_path + .parent() + .and_then(|p| p.parent()) // bin/ -> extension/ + .map(|p| p.to_path_buf()), + Err(_) => None, + }; + + let Some(extension_path) = extension_path else { + warn!( + "[Extension Dylint] Could not determine extension path, skipping detector initialization" + ); + return; + }; + + info!( + "[Extension Dylint] Initializing detectors from: {:?}", + extension_path + ); + + // Create or get the manager + let mut manager = match self.dylint_manager.lock().await.take() { + Some(m) => m, + None => match DylintDetectorManager::new() { + Ok(m) => m, + Err(e) => { + warn!("[Extension Dylint] Failed to create manager: {}", e); + return; + } + }, + }; + + manager.set_extension_path(extension_path); + + // Initialize (will check cache and compile if needed) + match manager.initialize().await { + Ok(compiled_paths) => { + if !compiled_paths.is_empty() { + info!( + "[Extension Dylint] Successfully initialized {} detector(s)", + compiled_paths.len() + ); + + // Add compiled detectors to dylint_runner + if let Some(dylint_runner) = &self.dylint_runner { + dylint_runner.add_workspace_detectors(compiled_paths); + info!("[Extension Dylint] Detectors added to dylint runner"); + } else { + warn!( + "[Extension Dylint] Dylint runner not available, cannot add detectors" + ); + } + } else { + info!("[Extension Dylint] No detectors found in extension"); + } + } + Err(e) => { + warn!("[Extension Dylint] Failed to initialize detectors: {}", e); + } + } + + // Store manager back + *self.dylint_manager.lock().await = Some(manager); + } + + fn try_init_dylint_runner() -> Option> { + // Get the extension path (parent of language-server binary) + let exe_path = std::env::current_exe().ok()?; + let extension_path = exe_path.parent()?.parent()?; // bin/ -> extension/ + + match DylintRunner::new(extension_path) { + Ok(runner) => { + // Runner can start empty and have detectors added later + info!("Dylint runner initialized successfully"); + if runner.is_available() { + info!("Pre-compiled lints loaded: {:?}", runner.loaded_lints()); + } else { + info!("No pre-compiled lints found, but runner ready for extension detectors"); + } + Some(Arc::new(runner)) + } + Err(e) => { + warn!( + "Failed to initialize dylint runner: {}. Dylint integration disabled.", + e + ); + None + } } } async fn on_change(&self, params: TextDocumentItem) { - // Run security analysis - let diagnostics = { + // 1. Run syn-based detectors (fast, real-time) + let syn_diagnostics = { let mut registry = self.detector_registry.lock().await; let file_path = params.uri.to_file_path().ok(); registry.analyze(¶ms.text, file_path.as_ref()) }; - // Publish diagnostics to the client + // 2. Publish syn-based diagnostics immediately self.client - .publish_diagnostics(params.uri.clone(), diagnostics, Some(params.version)) + .publish_diagnostics( + params.uri.clone(), + syn_diagnostics.clone(), + Some(params.version), + ) .await; + + // 3. Run dylint in background and merge diagnostics + if let Some(dylint_runner) = &self.dylint_runner + && let Some(workspace_root) = self.workspace_root.lock().await.as_ref() + { + let runner: Arc = Arc::clone(dylint_runner); + let workspace = workspace_root.clone(); + let uri = params.uri.clone(); + let client = self.client.clone(); + let version = params.version; + + tokio::spawn(async move { + info!("Running dylint lints on workspace: {}", workspace.display()); + match runner.run_lints(&workspace).await { + Ok(dylint_diagnostics) => { + info!( + "Dylint returned {} total diagnostics", + dylint_diagnostics.len() + ); + + // Filter diagnostics for this file + let file_path = uri.to_file_path().ok(); + let dylint_file_diagnostics: Vec<_> = dylint_diagnostics + .iter() + .filter(|d| { + let matches = file_path + .as_ref() + .map(|p| { + let path_str = p.to_string_lossy(); + let diagnostic_file = &d.file_name; + let result = path_str.ends_with(diagnostic_file) + || path_str.contains(diagnostic_file); + info!( + "Comparing {} with {} = {}", + path_str, diagnostic_file, result + ); + result + }) + .unwrap_or(false); + matches + }) + .map(|d| d.to_lsp_diagnostic()) + .collect(); + + info!( + "Filtered to {} diagnostics for this file", + dylint_file_diagnostics.len() + ); + + if !dylint_file_diagnostics.is_empty() { + info!( + "Publishing {} dylint issues for file", + dylint_file_diagnostics.len() + ); + + // Merge syn and dylint diagnostics + let mut merged_diagnostics = syn_diagnostics; + merged_diagnostics.extend(dylint_file_diagnostics); + + info!( + "Publishing {} total diagnostics (syn + dylint)", + merged_diagnostics.len() + ); + + // Publish merged diagnostics + client + .publish_diagnostics(uri, merged_diagnostics, Some(version)) + .await; + } + } + Err(e) => { + info!("Dylint failed: {}", e); + } + } + }); + } } /// Get information about all registered detectors @@ -360,7 +910,7 @@ pub struct DetectorStats { fn create_default_registry() -> DetectorRegistry { info!("Creating new detector registry with all detectors"); let registry = DetectorRegistryBuilder::new() - .with_detector(UnsafeMathDetector::default()) + // .with_detector(UnsafeMathDetector::default()) .with_detector(MissingSignerDetector::default()) // Ensure MissingSignerDetector is included .with_detector(ManualLamportsZeroingDetector::default()) .with_detector(SysvarAccountDetector::default()) diff --git a/language-server/src/core/dylint/cache.rs b/language-server/src/core/dylint/cache.rs new file mode 100644 index 0000000..653fe32 --- /dev/null +++ b/language-server/src/core/dylint/cache.rs @@ -0,0 +1,170 @@ +use crate::core::dylint::constants::REQUIRED_NIGHTLY_VERSION; +use crate::core::dylint::scanner::DylintDetectorInfo; +use anyhow::{Context, Result}; +use log::{debug, info}; +use std::collections::hash_map::DefaultHasher; +use std::fs; +use std::hash::{Hash, Hasher}; +use std::path::{Path, PathBuf}; + +/// Cache manager for compiled dylint detectors +pub struct DylintDetectorCache { + cache_dir: PathBuf, +} + +impl std::fmt::Debug for DylintDetectorCache { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("DylintDetectorCache") + .field("cache_dir", &self.cache_dir) + .finish() + } +} + +impl DylintDetectorCache { + /// Create a new cache manager + pub fn new() -> Result { + let cache_dir = Self::get_cache_directory()?; + + // Create cache directory if it doesn't exist + if !cache_dir.exists() { + fs::create_dir_all(&cache_dir).context("Failed to create cache directory")?; + info!("Created cache directory: {:?}", cache_dir); + } + + Ok(Self { cache_dir }) + } + + /// Get the cache directory path + fn get_cache_directory() -> Result { + let cache_base = dirs::cache_dir().context("Failed to get cache directory")?; + + Ok(cache_base.join("solana-vscode").join("dylint-detectors")) + } + + /// Get the cache key for a detector and nightly version + fn get_cache_key(detector: &DylintDetectorInfo, nightly_version: &str) -> String { + // Create a hash of the detector path and nightly version + let mut hasher = DefaultHasher::new(); + detector.crate_path.hash(&mut hasher); + detector.crate_name.hash(&mut hasher); + nightly_version.hash(&mut hasher); + format!("{:x}", hasher.finish()) + } + + /// Get the cached library path for a detector + pub fn get_cached_library( + &self, + detector: &DylintDetectorInfo, + nightly_version: &str, + ) -> Option { + // Try new format first (with nightly version in filename) + let extension = if cfg!(target_os = "macos") { + "dylib" + } else if cfg!(target_os = "windows") { + "dll" + } else { + "so" + }; + + // Always use the extension's required nightly version + let platform = std::env::consts::ARCH; + let os = match std::env::consts::OS { + "macos" => "apple-darwin", + "linux" => "unknown-linux-gnu", + "windows" => "pc-windows-msvc", + _ => "unknown", + }; + + let filename = format!( + "lib{}@{}-{}-{}.{}", + detector.crate_name.replace("-", "_"), + REQUIRED_NIGHTLY_VERSION, + platform, + os, + extension + ); + + let lib_path = self.cache_dir.join(&filename); + if lib_path.exists() { + debug!("Found cached library (new format): {:?}", lib_path); + return Some(lib_path); + } + + // Fallback: try old hash-based format + let cache_key = Self::get_cache_key(detector, nightly_version); + let cached_path = self.cache_dir.join(&cache_key); + let lib_path = cached_path.with_extension(extension); + if lib_path.exists() { + debug!("Found cached library (old format): {:?}", lib_path); + return Some(lib_path); + } + + None + } + + /// Store a compiled library in the cache + /// The filename includes the detector name and nightly version for easy identification + pub fn cache_library( + &self, + detector: &DylintDetectorInfo, + nightly_version: &str, + compiled_lib: &Path, + ) -> Result { + let extension = compiled_lib + .extension() + .and_then(|e| e.to_str()) + .unwrap_or("so"); + + // Always use the extension's required nightly version + // Create filename: lib@-. + // This format matches the pre-compiled lints and allows dylint runner to detect toolchain + let platform = std::env::consts::ARCH; + let os = match std::env::consts::OS { + "macos" => "apple-darwin", + "linux" => "unknown-linux-gnu", + "windows" => "pc-windows-msvc", + _ => "unknown", + }; + + let filename = format!( + "lib{}@{}-{}-{}.{}", + detector.crate_name.replace("-", "_"), + REQUIRED_NIGHTLY_VERSION, + platform, + os, + extension + ); + + let cached_path = self.cache_dir.join(filename); + + // Copy the compiled library to cache + fs::copy(compiled_lib, &cached_path).context("Failed to copy library to cache")?; + + info!("Cached library to: {:?}", cached_path); + Ok(cached_path) + } + + /// Check if a cached library exists and is valid + pub fn is_cached(&self, detector: &DylintDetectorInfo, nightly_version: &str) -> bool { + self.get_cached_library(detector, nightly_version).is_some() + } + + /// Clear the cache for a specific detector + pub fn clear_cache(&self, detector: &DylintDetectorInfo, nightly_version: &str) -> Result<()> { + if let Some(cached_path) = self.get_cached_library(detector, nightly_version) { + fs::remove_file(&cached_path).context("Failed to remove cached library")?; + info!("Cleared cache for detector: {}", detector.crate_name); + } + Ok(()) + } + + /// Clear all cached detectors + pub fn clear_all_cache(&self) -> Result<()> { + if self.cache_dir.exists() { + fs::remove_dir_all(&self.cache_dir).context("Failed to clear cache directory")?; + fs::create_dir_all(&self.cache_dir).context("Failed to recreate cache directory")?; + info!("Cleared all cached detectors"); + } + Ok(()) + } +} diff --git a/language-server/src/core/dylint/compiler.rs b/language-server/src/core/dylint/compiler.rs new file mode 100644 index 0000000..9e48374 --- /dev/null +++ b/language-server/src/core/dylint/compiler.rs @@ -0,0 +1,269 @@ +use crate::core::dylint::constants::REQUIRED_NIGHTLY_VERSION; +use crate::core::dylint::scanner::DylintDetectorInfo; +use anyhow::{Context, Result}; +use log::info; +use std::path::Path; +use std::process::Command; +use tokio::process::Command as TokioCommand; + +/// Compiler for dylint detector crates +pub struct DylintDetectorCompiler; + +impl std::fmt::Debug for DylintDetectorCompiler { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("DylintDetectorCompiler").finish() + } +} + +impl DylintDetectorCompiler { + pub fn new() -> Self { + Self + } + + /// Compile a dylint detector crate with the extension's required nightly Rust version + /// All detectors must be compatible with REQUIRED_NIGHTLY_VERSION + pub async fn compile_detector( + &self, + detector: &DylintDetectorInfo, + _nightly_version: &str, + ) -> Result { + info!( + "Compiling dylint detector {} with required nightly {}", + detector.crate_name, REQUIRED_NIGHTLY_VERSION + ); + + // Always use the extension's required nightly version + let toolchain_arg = format!("+{}", REQUIRED_NIGHTLY_VERSION); + + // Build PATH with cargo bin directories + let current_path = std::env::var("PATH").unwrap_or_default(); + let home = dirs::home_dir() + .ok_or_else(|| anyhow::anyhow!("Could not determine home directory"))?; + + let cargo_bin = home.join(".cargo").join("bin"); + let new_path = format!( + "{}:/usr/local/bin:/usr/bin:{}", + cargo_bin.display(), + current_path + ); + + // Build the detector using cargo with the required nightly + // Use debug mode for faster builds during development + let output = TokioCommand::new("cargo") + .arg(&toolchain_arg) + .arg("build") + // .arg("--release") // Commented out for faster development builds + .arg("--manifest-path") + .arg(&detector.cargo_toml_path) + .current_dir(&detector.crate_path) + .env("PATH", new_path) + .output() + .await + .context("Failed to execute cargo build")?; + + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + let stdout = String::from_utf8_lossy(&output.stdout); + log::error!("Detector compilation failed!"); + log::error!("stdout: {}", stdout); + log::error!("stderr: {}", stderr); + anyhow::bail!("Failed to compile detector: {}", stderr); + } + + // Find the compiled library file + let lib_path = self.find_compiled_library(&detector.crate_path, &detector.crate_name)?; + + info!("Successfully compiled detector to: {:?}", lib_path); + Ok(lib_path) + } + + /// Find the compiled library file (.so on Linux, .dylib on macOS, .dll on Windows) + fn find_compiled_library(&self, crate_path: &Path, crate_name: &str) -> Result { + // Check both debug and release directories (debug is default now) + let debug_dir = crate_path.join("target").join("debug"); + let release_dir = crate_path.join("target").join("release"); + + // Try debug first (faster builds), then release + let target_dir = if debug_dir.exists() { + debug_dir + } else { + release_dir + }; + + // Try different library extensions + let extensions = if cfg!(target_os = "macos") { + vec!["dylib"] + } else if cfg!(target_os = "windows") { + vec!["dll"] + } else { + vec!["so"] + }; + + for ext in extensions { + let lib_name = format!("lib{}.{}", crate_name.replace("-", "_"), ext); + let lib_path = target_dir.join(&lib_name); + if lib_path.exists() { + return Ok(lib_path); + } + + // Also try without lib prefix (Windows) + let lib_name = format!("{}.{}", crate_name.replace("-", "_"), ext); + let lib_path = target_dir.join(&lib_name); + if lib_path.exists() { + return Ok(lib_path); + } + } + + anyhow::bail!( + "Could not find compiled library for {} in {:?}", + crate_name, + target_dir + ) + } + + /// Check if the required nightly Rust version is available + pub fn is_nightly_available() -> bool { + use log::{debug, warn}; + + debug!("[Nightly Check] Attempting to check nightly availability..."); + + // Build PATH with cargo bin directories (same as dylint runner) + let current_path = std::env::var("PATH").unwrap_or_default(); + let home = match dirs::home_dir() { + Some(h) => h, + None => { + warn!("[Nightly Check] Could not determine home directory"); + return false; + } + }; + + let cargo_bin = home.join(".cargo").join("bin"); + let rustup_bin = home.join(".rustup").join("toolchains"); + let new_path = format!( + "{}:{}:/usr/local/bin:/usr/bin:{}", + cargo_bin.display(), + rustup_bin.display(), + current_path + ); + + debug!("[Nightly Check] Using PATH: {}", new_path); + debug!( + "[Nightly Check] Checking for required nightly: {}", + REQUIRED_NIGHTLY_VERSION + ); + + match Command::new("rustc") + .arg(format!("+{}", REQUIRED_NIGHTLY_VERSION)) + .arg("--version") + .env("PATH", new_path) + .output() + { + Ok(output) => { + let success = output.status.success(); + if success { + let version = String::from_utf8_lossy(&output.stdout); + debug!("[Nightly Check] Success! Version: {}", version.trim()); + } else { + let stderr = String::from_utf8_lossy(&output.stderr); + warn!("[Nightly Check] Command failed. stderr: {}", stderr); + } + success + } + Err(e) => { + warn!("[Nightly Check] Failed to execute rustc: {}", e); + false + } + } + } + + /// Get the required nightly Rust version (returns the constant) + /// This ensures all detectors use the same nightly version + pub fn get_nightly_version() -> Result { + // Simply return the required version - we don't query rustc + Ok(REQUIRED_NIGHTLY_VERSION.to_string()) + } + + /// Get the actual installed nightly version info (for logging) + pub fn get_installed_nightly_info() -> Result { + // Build PATH with cargo bin directories (same as nightly check) + let current_path = std::env::var("PATH").unwrap_or_default(); + let home = dirs::home_dir() + .ok_or_else(|| anyhow::anyhow!("Could not determine home directory"))?; + + let cargo_bin = home.join(".cargo").join("bin"); + let rustup_bin = home.join(".rustup").join("toolchains"); + let new_path = format!( + "{}:{}:/usr/local/bin:/usr/bin:{}", + cargo_bin.display(), + rustup_bin.display(), + current_path + ); + + let output = Command::new("rustc") + .arg(format!("+{}", REQUIRED_NIGHTLY_VERSION)) + .arg("--version") + .env("PATH", new_path) + .output() + .context("Failed to execute rustc")?; + + if !output.status.success() { + anyhow::bail!("Failed to get installed nightly info"); + } + + let version = String::from_utf8_lossy(&output.stdout); + // Extract version string (e.g., "rustc 1.75.0-nightly (abc123 2024-01-01)") + let version = version.trim(); + Ok(version.to_string()) + } + + /// Check if dylint-driver is installed for the required nightly version + pub fn is_dylint_driver_available() -> bool { + use log::{debug, warn}; + + debug!("[Dylint Driver Check] Checking for dylint-driver..."); + + let home = match dirs::home_dir() { + Some(h) => h, + None => { + warn!("[Dylint Driver Check] Could not determine home directory"); + return false; + } + }; + + let arch = std::env::consts::ARCH; + let os = match std::env::consts::OS { + "macos" => "apple-darwin", + "linux" => "unknown-linux-gnu", + _ => "unknown", + }; + + let toolchain_target = format!("{}-{}-{}", REQUIRED_NIGHTLY_VERSION, arch, os); + let dylint_driver = home + .join(".dylint_drivers") + .join(&toolchain_target) + .join("dylint-driver"); + + let exists = dylint_driver.exists(); + if exists { + debug!( + "[Dylint Driver Check] Found dylint-driver at: {}", + dylint_driver.display() + ); + } else { + debug!( + "[Dylint Driver Check] dylint-driver not found at: {}", + dylint_driver.display() + ); + } + + exists + } +} + +impl Default for DylintDetectorCompiler { + fn default() -> Self { + Self::new() + } +} + +use std::path::PathBuf; diff --git a/language-server/src/core/dylint/constants.rs b/language-server/src/core/dylint/constants.rs new file mode 100644 index 0000000..c800e1d --- /dev/null +++ b/language-server/src/core/dylint/constants.rs @@ -0,0 +1,7 @@ +/// The specific nightly Rust version required by this extension +/// All dylint detectors must be compatible with this version +pub const REQUIRED_NIGHTLY_VERSION: &str = "nightly-2025-09-18"; + +/// Components required for building dylint detectors +pub const REQUIRED_COMPONENTS: &[&str] = &["llvm-tools-preview", "rustc-dev"]; + diff --git a/language-server/src/core/dylint/loader.rs b/language-server/src/core/dylint/loader.rs new file mode 100644 index 0000000..a4c724b --- /dev/null +++ b/language-server/src/core/dylint/loader.rs @@ -0,0 +1,87 @@ +use anyhow::{Context, Result}; +use libloading::Library; +use log::{info, warn}; +use std::path::Path; + +/// Loader for dynamically loading compiled dylint detectors +pub struct DylintDetectorLoader { + libraries: Vec, +} + +impl std::fmt::Debug for DylintDetectorLoader { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("DylintDetectorLoader") + .field("library_count", &self.libraries.len()) + .finish() + } +} + +impl DylintDetectorLoader { + pub fn new() -> Self { + Self { + libraries: Vec::new(), + } + } + + /// Load a compiled dylint detector library + pub unsafe fn load_detector(&mut self, lib_path: &Path) -> Result<()> { + info!("Loading dylint detector from: {:?}", lib_path); + + let library = unsafe { Library::new(lib_path) } + .context(format!("Failed to load library from {:?}", lib_path))?; + + // Try to find and call the detector registration function + // Dylint detectors typically export a function like `register_detector` or similar + // This is a placeholder - actual implementation depends on dylint detector API + match unsafe { self.register_detector_from_library(&library) } { + Ok(_) => { + info!("Successfully loaded detector from {:?}", lib_path); + self.libraries.push(library); + Ok(()) + } + Err(e) => { + warn!("Failed to register detector from {:?}: {}", lib_path, e); + Err(e) + } + } + } + + /// Register a detector from a loaded library + unsafe fn register_detector_from_library(&self, library: &Library) -> Result<()> { + // Try to find the registration function + // The exact function name depends on the dylint detector implementation + // Common patterns: "register", "init", "setup" + let func_names = ["register", "init", "setup", "register_detector"]; + + for func_name in &func_names { + if let Ok(symbol) = unsafe { library.get::(func_name.as_bytes()) } { + // Call the registration function + symbol(); + return Ok(()); + } + } + + // If no registration function found, that's okay - the detector might be loaded differently + // Just log a warning and continue + warn!("No registration function found in library, assuming auto-registration"); + Ok(()) + } + + /// Unload all loaded detectors + pub fn unload_all(&mut self) { + info!("Unloading {} detector library(ies)", self.libraries.len()); + self.libraries.clear(); + } +} + +impl Default for DylintDetectorLoader { + fn default() -> Self { + Self::new() + } +} + +impl Drop for DylintDetectorLoader { + fn drop(&mut self) { + self.unload_all(); + } +} diff --git a/language-server/src/core/dylint/manager.rs b/language-server/src/core/dylint/manager.rs new file mode 100644 index 0000000..e5702bc --- /dev/null +++ b/language-server/src/core/dylint/manager.rs @@ -0,0 +1,217 @@ +use crate::core::dylint::{ + cache::DylintDetectorCache, compiler::DylintDetectorCompiler, scanner::DylintDetectorScanner, +}; +use anyhow::{Context, Result}; +use log::{info, warn}; +use std::path::PathBuf; +use std::sync::Arc; +use tokio::sync::Mutex; + +/// Manager for dylint detectors - handles scanning, compilation, and caching +/// Compiled detectors are added to dylint_runner which runs them via cargo +nightly dylint +#[derive(Debug)] +pub struct DylintDetectorManager { + scanner: DylintDetectorScanner, + compiler: DylintDetectorCompiler, + cache: Arc>, + nightly_version: Option, + /// Whether detectors have been initialized (compiled/cached) + initialized: bool, + /// Cached list of compiled detector paths + compiled_paths: Vec, +} + +impl DylintDetectorManager { + pub fn new() -> Result { + let cache = Arc::new(Mutex::new(DylintDetectorCache::new()?)); + + Ok(Self { + scanner: DylintDetectorScanner::new(), + compiler: DylintDetectorCompiler::new(), + cache, + nightly_version: None, + initialized: false, + compiled_paths: Vec::new(), + }) + } + + /// Check if nightly Rust is available + pub fn check_nightly_available() -> bool { + DylintDetectorCompiler::is_nightly_available() + } + + /// Check if dylint-driver is available + pub fn check_dylint_driver_available() -> bool { + DylintDetectorCompiler::is_dylint_driver_available() + } + + /// Check if detectors have been initialized + pub fn is_initialized(&self) -> bool { + self.initialized + } + + /// Set the extension path (where bundled detectors are located) + pub fn set_extension_path(&mut self, extension_path: PathBuf) { + self.scanner.set_extension_path(extension_path); + } + + /// Pre-build all detectors (compile and cache them for future reuse) + /// This can be called separately to build detectors before they're needed + pub async fn prebuild_detectors(&mut self) -> Result<()> { + // Get nightly version + let nightly_version = DylintDetectorCompiler::get_nightly_version() + .context("Failed to get nightly Rust version. Make sure nightly is installed.")?; + + self.nightly_version = Some(nightly_version.clone()); + info!( + "Pre-building dylint detectors with nightly: {}", + nightly_version + ); + + // Scan for detectors + let detectors = self.scanner.scan_detectors(); + + if detectors.is_empty() { + info!("No dylint detectors found in workspace"); + return Ok(()); + } + + info!("Pre-building {} dylint detector(s)...", detectors.len()); + + // Build and cache each detector (but don't load yet) + for detector in detectors { + if let Err(e) = self + .build_and_cache_detector(&detector, &nightly_version) + .await + { + warn!( + "Failed to pre-build detector {}: {}", + detector.crate_name, e + ); + } + } + + Ok(()) + } + + /// Initialize and compile all dylint detectors (reuse cached builds if available) + /// Returns the paths to compiled detector libraries + /// This is the main initialization method called on first save + pub async fn initialize(&mut self) -> Result> { + // If already initialized, return cached paths + if self.initialized { + info!("[Extension Dylint] Detectors already initialized, returning cached paths"); + return Ok(self.compiled_paths.clone()); + } + + // Get nightly version + let nightly_version = DylintDetectorCompiler::get_nightly_version() + .context("Failed to get nightly Rust version. Make sure nightly is installed.")?; + + self.nightly_version = Some(nightly_version.clone()); + info!( + "[Extension Dylint] Initializing dylint detectors with nightly: {}", + nightly_version + ); + + // Scan for detectors in extension/detectors/ + let detectors = self.scanner.scan_detectors(); + + if detectors.is_empty() { + info!("[Extension Dylint] No dylint detectors found in extension"); + self.initialized = true; // Mark as initialized even if empty + return Ok(Vec::new()); + } + + info!( + "[Extension Dylint] Found {} dylint detector(s), checking cache or compiling...", + detectors.len() + ); + + // Compile each detector (will reuse cached builds) and collect paths + let mut compiled_paths = Vec::new(); + for detector in detectors { + match self + .build_and_cache_detector(&detector, &nightly_version) + .await + { + Ok(path) => { + compiled_paths.push(path); + } + Err(e) => { + warn!("Failed to compile detector {}: {}", detector.crate_name, e); + } + } + } + + // Mark as initialized and cache paths + self.initialized = true; + self.compiled_paths = compiled_paths.clone(); + + info!( + "[Extension Dylint] Successfully initialized {} detector(s)", + compiled_paths.len() + ); + Ok(compiled_paths) + } + + /// Build and cache a detector (without loading it) + async fn build_and_cache_detector( + &self, + detector: &crate::core::dylint::scanner::DylintDetectorInfo, + nightly_version: &str, + ) -> Result { + let cache = self.cache.lock().await; + + // Check if already cached - if so, just return the cached path + if let Some(cached) = cache.get_cached_library(detector, nightly_version) { + info!( + "Detector {} already cached, skipping build", + detector.crate_name + ); + return Ok(cached); + } + + // Not cached - compile it + drop(cache); + info!( + "Building detector: {} with nightly {}", + detector.crate_name, nightly_version + ); + + let compiled = self + .compiler + .compile_detector(detector, nightly_version) + .await + .context("Failed to compile detector")?; + + // Cache the compiled version for future reuse + let cache = self.cache.lock().await; + let cached_path = cache + .cache_library(detector, nightly_version, &compiled) + .context("Failed to cache compiled detector")?; + + info!( + "Built and cached detector for future reuse: {:?}", + cached_path + ); + Ok(cached_path) + } + + /// Reload all detectors (useful after workspace changes) + pub async fn reload(&mut self) -> Result> { + // Reinitialize (will reuse cache) + self.initialize().await + } + + /// Get the current nightly version + pub fn nightly_version(&self) -> Option<&str> { + self.nightly_version.as_deref() + } +} + +impl Default for DylintDetectorManager { + fn default() -> Self { + Self::new().expect("Failed to create DylintDetectorManager") + } +} diff --git a/language-server/src/core/dylint/mod.rs b/language-server/src/core/dylint/mod.rs new file mode 100644 index 0000000..0b81d32 --- /dev/null +++ b/language-server/src/core/dylint/mod.rs @@ -0,0 +1,12 @@ +pub mod cache; +pub mod compiler; +pub mod constants; +pub mod loader; +pub mod manager; +pub mod scanner; + +pub use cache::*; +pub use compiler::*; +pub use loader::*; +pub use manager::*; +pub use scanner::*; diff --git a/language-server/src/core/dylint/scanner.rs b/language-server/src/core/dylint/scanner.rs new file mode 100644 index 0000000..7fc1a63 --- /dev/null +++ b/language-server/src/core/dylint/scanner.rs @@ -0,0 +1,164 @@ +use log::{debug, info, warn}; +use std::fs; +use std::path::{Path, PathBuf}; + +/// Information about a dylint detector crate +#[derive(Debug, Clone)] +pub struct DylintDetectorInfo { + pub crate_path: PathBuf, + pub crate_name: String, + pub cargo_toml_path: PathBuf, +} + +/// Scanner for finding dylint detector crates in the workspace +#[derive(Debug)] +pub struct DylintDetectorScanner { + workspace_root: Option, +} + +impl DylintDetectorScanner { + pub fn new() -> Self { + Self { + workspace_root: None, + } + } + + pub fn set_workspace_root(&mut self, root: PathBuf) { + self.workspace_root = Some(root); + } + + /// Set the extension path (where bundled detectors are located) + pub fn set_extension_path(&mut self, extension_path: PathBuf) { + // Look for detectors in extension/detectors/ + self.workspace_root = Some(extension_path.join("detectors")); + } + + /// Scan for dylint detector crates in the workspace + pub fn scan_detectors(&self) -> Vec { + let Some(root) = &self.workspace_root else { + warn!("No workspace root set, cannot scan for dylint detectors"); + return Vec::new(); + }; + + info!( + "[Workspace Dylint] Scanning for dylint detector crates in: {:?}", + root + ); + let mut detectors = Vec::new(); + + // Look for Cargo.toml files that might be dylint detectors + if let Ok(cargo_files) = self.find_cargo_toml_files(root) { + for cargo_toml in cargo_files { + if let Some(detector_info) = self.check_if_dylint_detector(&cargo_toml) { + info!( + "Found dylint detector: {} at {:?}", + detector_info.crate_name, detector_info.crate_path + ); + detectors.push(detector_info); + } + } + } + + info!( + "[Workspace Dylint] Found {} dylint detector(s)", + detectors.len() + ); + detectors + } + + /// Find all Cargo.toml files in the workspace + fn find_cargo_toml_files(&self, root: &Path) -> Result, std::io::Error> { + let mut files = Vec::new(); + self.find_cargo_toml_recursive(root, &mut files)?; + Ok(files) + } + + fn find_cargo_toml_recursive( + &self, + dir: &Path, + files: &mut Vec, + ) -> Result<(), std::io::Error> { + if !dir.is_dir() { + return Ok(()); + } + + for entry in fs::read_dir(dir)? { + let entry = entry?; + let path = entry.path(); + + if path.is_dir() { + // Skip common directories + if let Some(dir_name) = path.file_name().and_then(|n| n.to_str()) { + if matches!( + dir_name, + "target" | "node_modules" | ".git" | ".vscode" | "out" | ".anchor" + ) { + continue; + } + } + self.find_cargo_toml_recursive(&path, files)?; + } else if path.file_name().and_then(|n| n.to_str()) == Some("Cargo.toml") { + files.push(path); + } + } + + Ok(()) + } + + /// Check if a Cargo.toml represents a dylint detector + fn check_if_dylint_detector(&self, cargo_toml: &Path) -> Option { + let content = match fs::read_to_string(cargo_toml) { + Ok(c) => c, + Err(e) => { + debug!("Failed to read {:?}: {}", cargo_toml, e); + return None; + } + }; + + // Check if it's a dylint detector: + // 1. Has dylint as a dependency + // 2. Has [lib] section with crate-type = ["dylib"] + // 3. Or has proc-macro = true (some dylint detectors use proc macros) + let is_dylint = content.contains("dylint") + && (content.contains("crate-type = [\"dylib\"]") + || content.contains("crate-type = [\"cdylib\"]") + || content.contains("proc-macro = true")); + + if !is_dylint { + return None; + } + + // Extract crate name from Cargo.toml + let crate_name = self.extract_crate_name(&content)?; + let crate_path = cargo_toml.parent()?.to_path_buf(); + + Some(DylintDetectorInfo { + crate_path, + crate_name, + cargo_toml_path: cargo_toml.to_path_buf(), + }) + } + + /// Extract crate name from Cargo.toml content + fn extract_crate_name(&self, content: &str) -> Option { + // Look for [package] name = "..." + for line in content.lines() { + let line = line.trim(); + if line.starts_with("name =") { + if let Some(name) = line.strip_prefix("name =") { + let name = name.trim(); + // Remove quotes + let name = name.trim_matches('"').trim_matches('\''); + return Some(name.to_string()); + } + } + } + None + } +} + +impl Default for DylintDetectorScanner { + fn default() -> Self { + Self::new() + } +} diff --git a/language-server/src/core/mod.rs b/language-server/src/core/mod.rs index 7b1a523..bfecb98 100644 --- a/language-server/src/core/mod.rs +++ b/language-server/src/core/mod.rs @@ -1,11 +1,16 @@ pub mod backend_stats; pub mod detectors; +pub mod dylint; pub mod file_scanner; pub mod notifications; pub mod registry; pub mod utilities; pub use detectors::*; +pub use dylint::{ + DylintDetectorCache, DylintDetectorCompiler, DylintDetectorLoader, DylintDetectorManager, + DylintDetectorScanner, +}; pub use file_scanner::*; pub use notifications::*; pub use registry::*; diff --git a/language-server/src/core/notifications.rs b/language-server/src/core/notifications.rs index e33988c..dedcee1 100644 --- a/language-server/src/core/notifications.rs +++ b/language-server/src/core/notifications.rs @@ -30,6 +30,15 @@ impl tower_lsp::lsp_types::notification::Notification for FileAnalysisNotificati const METHOD: &'static str = "solana/fileAnalysis"; } +/// Custom notification for detector status updates +#[derive(Debug)] +pub enum DetectorStatusNotification {} + +impl tower_lsp::lsp_types::notification::Notification for DetectorStatusNotification { + type Params = DetectorStatus; + const METHOD: &'static str = "solana/detectorStatus"; +} + /// Summary of scan results to send to the extension #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ScanSummary { @@ -96,3 +105,10 @@ pub struct FileAnalysisResult { pub is_test_file: bool, pub analysis_time_ms: u64, } + +/// Status of detector operations +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct DetectorStatus { + pub status: String, // "initializing", "building", "running", "complete", "idle" + pub message: String, +} diff --git a/language-server/src/dylint_runner/diagnostics.rs b/language-server/src/dylint_runner/diagnostics.rs new file mode 100644 index 0000000..c01f747 --- /dev/null +++ b/language-server/src/dylint_runner/diagnostics.rs @@ -0,0 +1,49 @@ +use serde::{Deserialize, Serialize}; + +/// A diagnostic message from dylint +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct DylintDiagnostic { + pub file_name: String, + pub line_start: usize, + pub line_end: usize, + pub column_start: usize, + pub column_end: usize, + pub message: String, + pub code: String, + pub level: String, +} + +impl DylintDiagnostic { + /// Convert to LSP Diagnostic + pub fn to_lsp_diagnostic(&self) -> tower_lsp::lsp_types::Diagnostic { + use tower_lsp::lsp_types::{Diagnostic, DiagnosticSeverity, Position, Range}; + + let severity = match self.level.as_str() { + "error" => Some(DiagnosticSeverity::ERROR), + "warning" => Some(DiagnosticSeverity::WARNING), + "note" | "help" => Some(DiagnosticSeverity::INFORMATION), + _ => Some(DiagnosticSeverity::WARNING), + }; + + // LSP uses 0-based indexing, dylint uses 1-based + let start = Position { + line: (self.line_start.saturating_sub(1)) as u32, + character: (self.column_start.saturating_sub(1)) as u32, + }; + let end = Position { + line: (self.line_end.saturating_sub(1)) as u32, + character: (self.column_end.saturating_sub(1)) as u32, + }; + + Diagnostic { + range: Range { start, end }, + severity, + code: Some(tower_lsp::lsp_types::NumberOrString::String( + self.code.clone(), + )), + source: Some("solana".to_string()), + message: self.message.clone(), + ..Default::default() + } + } +} diff --git a/language-server/src/dylint_runner/mod.rs b/language-server/src/dylint_runner/mod.rs new file mode 100644 index 0000000..50c1967 --- /dev/null +++ b/language-server/src/dylint_runner/mod.rs @@ -0,0 +1,7 @@ +mod diagnostics; +mod parser; +mod runner; + +pub use diagnostics::DylintDiagnostic; +pub use runner::DylintRunner; + diff --git a/language-server/src/dylint_runner/parser.rs b/language-server/src/dylint_runner/parser.rs new file mode 100644 index 0000000..569a599 --- /dev/null +++ b/language-server/src/dylint_runner/parser.rs @@ -0,0 +1,133 @@ +use super::diagnostics::DylintDiagnostic; +use anyhow::{Context, Result}; +use serde_json::Value; + +/// Parse cargo check JSON output and extract lint diagnostics +/// Only accepts diagnostics from the specified lint codes (whitelist approach) +pub fn parse_json_output( + stdout: &str, + allowed_lint_codes: &[String], +) -> Result> { + let mut diagnostics = Vec::new(); + + for line in stdout.lines() { + // Skip empty lines + if line.trim().is_empty() { + continue; + } + + // Parse JSON + let json: Value = match serde_json::from_str(line) { + Ok(v) => v, + Err(_) => continue, // Skip non-JSON lines + }; + + // Filter for compiler messages + if json.get("reason").and_then(|r| r.as_str()) != Some("compiler-message") { + continue; + } + + // Extract message + let message = match json.get("message") { + Some(m) => m, + None => continue, + }; + + // Check if it has a code (lints have codes) + let code = message + .get("code") + .and_then(|c| c.get("code")) + .and_then(|c| c.as_str()); + + // WHITELIST: Only accept diagnostics from our loaded lints + if let Some(code) = code + && allowed_lint_codes + .iter() + .any(|allowed| code.contains(allowed)) + { + // Parse into DylintDiagnostic + if let Ok(diagnostic) = parse_diagnostic(message) { + diagnostics.push(diagnostic); + } + } + } + + Ok(diagnostics) +} + +/// Parse a single diagnostic message +fn parse_diagnostic(message: &Value) -> Result { + // Get primary span + let spans = message + .get("spans") + .and_then(|s| s.as_array()) + .context("No spans in message")?; + + let primary_span = spans + .iter() + .find(|s| s.get("is_primary") == Some(&Value::Bool(true))) + .context("No primary span found")?; + + // Skip macro expansions + if primary_span.get("expansion").is_some() && !primary_span.get("expansion").unwrap().is_null() + { + anyhow::bail!("Diagnostic from macro expansion (filtered)"); + } + + // Extract fields + let file_name = primary_span + .get("file_name") + .and_then(|f| f.as_str()) + .context("No file_name")? + .to_string(); + + let line_start = primary_span + .get("line_start") + .and_then(|l| l.as_u64()) + .context("No line_start")? as usize; + + let line_end = primary_span + .get("line_end") + .and_then(|l| l.as_u64()) + .context("No line_end")? as usize; + + let column_start = primary_span + .get("column_start") + .and_then(|c| c.as_u64()) + .context("No column_start")? as usize; + + let column_end = primary_span + .get("column_end") + .and_then(|c| c.as_u64()) + .context("No column_end")? as usize; + + let msg = message + .get("message") + .and_then(|m| m.as_str()) + .context("No message text")? + .to_string(); + + let code = message + .get("code") + .and_then(|c| c.get("code")) + .and_then(|c| c.as_str()) + .context("No code")? + .to_string(); + + let level = message + .get("level") + .and_then(|l| l.as_str()) + .context("No level")? + .to_string(); + + Ok(DylintDiagnostic { + file_name, + line_start, + line_end, + column_start, + column_end, + message: msg, + code, + level, + }) +} diff --git a/language-server/src/dylint_runner/runner.rs b/language-server/src/dylint_runner/runner.rs new file mode 100644 index 0000000..cd9cd99 --- /dev/null +++ b/language-server/src/dylint_runner/runner.rs @@ -0,0 +1,334 @@ +use super::diagnostics::DylintDiagnostic; +use super::parser::parse_json_output; +use anyhow::{Context, Result}; +use log::{debug, info, warn}; +use std::path::{Path, PathBuf}; +use std::process::Stdio; +use std::sync::Arc; +use tokio::sync::Mutex; + +#[derive(Debug)] +pub struct DylintRunner { + /// Path to pre-compiled lint libraries (e.g., lints_compiled/macos-arm64/) + lint_libs_dir: PathBuf, + + /// List of lint library files to load (pre-compiled + workspace detectors) + lint_libs: Arc>>, + + /// Cache of last run results per workspace + cache: Arc>>>, +} + +impl DylintRunner { + /// Add workspace detector libraries to the runner + pub fn add_workspace_detectors(&self, detector_libs: Vec) { + let mut libs = self.lint_libs.lock().unwrap(); + info!("Adding {} workspace detector(s) to dylint runner", detector_libs.len()); + libs.extend(detector_libs); + info!("Dylint runner now has {} total lint(s)", libs.len()); + } + + /// Initialize the runner with pre-compiled lints (if available) + /// Can start empty and have detectors added later via add_workspace_detectors + pub fn new(extension_path: &Path) -> Result { + // 1. Detect platform + let platform = Self::detect_platform()?; + + // 2. Find pre-compiled lints directory + let lint_libs_dir = extension_path.join("lints_compiled").join(platform); + + // 3. Discover all .dylib/.so files in the directory (if it exists) + let lint_libs = if lint_libs_dir.exists() { + match Self::discover_lint_libs(&lint_libs_dir) { + Ok(libs) => { + if libs.is_empty() { + info!( + "No pre-compiled lints found in {}. Runner will start empty.", + lint_libs_dir.display() + ); + Vec::new() + } else { + info!( + "Dylint runner initialized with {} pre-compiled lints from {}", + libs.len(), + lint_libs_dir.display() + ); + libs + } + } + Err(e) => { + warn!("Failed to discover pre-compiled lints: {}. Starting with empty runner.", e); + Vec::new() + } + } + } else { + info!( + "Pre-compiled lints directory not found: {}. Runner will start empty and can have detectors added later.", + lint_libs_dir.display() + ); + Vec::new() + }; + + Ok(Self { + lint_libs_dir, + lint_libs: Arc::new(std::sync::Mutex::new(lint_libs)), + cache: Arc::new(Mutex::new(std::collections::HashMap::new())), + }) + } + + /// Run lints on a workspace + pub async fn run_lints(&self, workspace_path: &Path) -> Result> { + // Clone the lint libs list while holding the lock, then release it + let lint_libs: Vec = { + let libs = self.lint_libs.lock().unwrap(); + if libs.is_empty() { + return Ok(Vec::new()); + } + libs.clone() + }; + + debug!( + "Running dylint lints on workspace: {}", + workspace_path.display() + ); + + // Check if workspace has Cargo.toml + let cargo_toml = workspace_path.join("Cargo.toml"); + if !cargo_toml.exists() { + debug!("No Cargo.toml found in workspace, skipping dylint"); + return Ok(Vec::new()); + } + + // Build PATH with cargo bin directories + let current_path = std::env::var("PATH").unwrap_or_default(); + let home = dirs::home_dir() + .ok_or_else(|| anyhow::anyhow!("Could not determine home directory"))?; + + let cargo_bin = home.join(".cargo").join("bin"); + let new_path = format!( + "{}:/usr/local/bin:/usr/bin:{current_path}", + cargo_bin.display() + ); + + // Detect toolchain from lint's rust-toolchain file + let toolchain = Self::detect_lint_toolchain(&self.lint_libs_dir)?; + debug!("Using toolchain: {}", toolchain); + + // Get dylint-driver path + let dylint_driver = Self::find_dylint_driver(&home, &toolchain)?; + debug!("Using dylint-driver: {}", dylint_driver.display()); + + // Build DYLINT_LIBS JSON array with absolute paths + let dylint_libs_json = serde_json::to_string( + &lint_libs + .iter() + .map(|p| p.to_string_lossy().to_string()) + .collect::>(), + )?; + + debug!("DYLINT_LIBS: {}", dylint_libs_json); + + // Run cargo check with dylint + let output = tokio::process::Command::new("cargo") + .arg(format!("+{}", toolchain)) + .args(&["check", "--workspace", "--message-format=json"]) + .current_dir(workspace_path) + .env("PATH", new_path) + .env("RUSTC_WORKSPACE_WRAPPER", &dylint_driver) + .env("DYLINT_LIBS", dylint_libs_json) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .output() + .await + .context("Failed to spawn cargo check")?; + + // Extract lint names from loaded libraries + let stdout = String::from_utf8_lossy(&output.stdout); + let stderr = String::from_utf8_lossy(&output.stderr); + + // Debug: log cargo output + debug!("[Dylint] Cargo stdout length: {} bytes", stdout.len()); + debug!("[Dylint] Cargo stderr length: {} bytes", stderr.len()); + if !stderr.is_empty() { + debug!("[Dylint] Cargo stderr: {}", stderr.lines().take(10).collect::>().join("\n")); + } + + let lint_codes: Vec = lint_libs + .iter() + .filter_map(|path| { + path.file_stem() + .and_then(|s| s.to_str()) + .and_then(|s| s.split('@').next()) + .map(|s| s.strip_prefix("lib").unwrap_or(s).to_string()) + }) + .collect(); + + // Parse JSON output + debug!("[Dylint] Parsing output for lint codes: {:?}", lint_codes); + let diagnostics = parse_json_output(&stdout, &lint_codes)?; + debug!("[Dylint] Parsed {} diagnostic(s)", diagnostics.len()); + + // Update cache + { + let mut cache = self.cache.lock().await; + cache.insert(workspace_path.to_path_buf(), diagnostics.clone()); + } + + info!("Dylint found {} issues", diagnostics.len()); + Ok(diagnostics) + } + + /// Detect current platform + fn detect_platform() -> Result<&'static str> { + match (std::env::consts::OS, std::env::consts::ARCH) { + ("macos", "aarch64") => Ok("macos-arm64"), + ("macos", "x86_64") => Ok("macos-x64"), + ("linux", "x86_64") => Ok("linux-x64"), + ("linux", "aarch64") => Ok("linux-arm64"), + (os, arch) => Err(anyhow::anyhow!( + "Unsupported platform: {}-{}. \ + Supported: macos-arm64, macos-x64, linux-x64, linux-arm64", + os, + arch + )), + } + } + + /// Detect the Rust toolchain from the lint library filename + fn detect_lint_toolchain(lints_dir: &Path) -> Result { + use crate::core::dylint::constants::REQUIRED_NIGHTLY_VERSION; + + // Simply return the required nightly version - all detectors use this version + info!("Using extension's required nightly version: {}", REQUIRED_NIGHTLY_VERSION); + return Ok(REQUIRED_NIGHTLY_VERSION.to_string()); + + // Old detection code kept as fallback (commented out) + /* + // Extract toolchain from lint library filename + // Format: libunchecked_math@nightly-2025-09-18-aarch64-apple-darwin.dylib + if let Ok(entries) = std::fs::read_dir(lints_dir) { + for entry in entries.filter_map(|e| e.ok()) { + let file_name = entry.file_name(); + let file_name_str = file_name.to_string_lossy(); + + // Look for @ in filename + if let Some(at_pos) = file_name_str.find('@') { + let after_at = &file_name_str[at_pos + 1..]; + + // Extract toolchain: "nightly-2025-09-18-aarch64..." -> "nightly-2025-09-18" + // Date format is YYYY-MM-DD, so we need 4 parts: nightly-YYYY-MM-DD + let parts: Vec<&str> = after_at.split('-').collect(); + if parts.len() >= 5 && parts[0] == "nightly" { + // parts[0] = "nightly", parts[1] = "2025", parts[2] = "09", parts[3] = "18", parts[4] = "aarch64" + let toolchain = format!("{}-{}-{}-{}", parts[0], parts[1], parts[2], parts[3]); + info!("Detected toolchain from lint filename: {}", toolchain); + return Ok(toolchain); + } + } + } + } + */ + + // Fallback: Look for rust-toolchain file + let lints_parent = lints_dir + .parent() + .and_then(|p| p.parent()) + .ok_or_else(|| anyhow::anyhow!("Could not find lints directory"))? + .join("lints"); + + if let Ok(entries) = std::fs::read_dir(&lints_parent) { + for entry in entries.filter_map(|e| e.ok()) { + let rust_toolchain = entry.path().join("rust-toolchain"); + if rust_toolchain.exists() + && let Ok(content) = std::fs::read_to_string(&rust_toolchain) + { + for line in content.lines() { + if line.trim().starts_with("channel") + && let Some(channel) = line.split('"').nth(1) + { + info!("Detected toolchain from rust-toolchain file: {}", channel); + return Ok(channel.to_string()); + } + } + } + } + } + + // This code is now unreachable since we return early above + // But kept for safety + warn!("Could not detect toolchain, falling back to required nightly"); + Ok(REQUIRED_NIGHTLY_VERSION.to_string()) + } + + /// Find dylint-driver executable + fn find_dylint_driver(home: &Path, toolchain: &str) -> Result { + let arch = std::env::consts::ARCH; + let os = match std::env::consts::OS { + "macos" => "apple-darwin", + "linux" => "unknown-linux-gnu", + _ => "unknown", + }; + + let toolchain_target = format!("{}-{}-{}", toolchain, arch, os); + let dylint_driver = home + .join(".dylint_drivers") + .join(&toolchain_target) + .join("dylint-driver"); + + if !dylint_driver.exists() { + anyhow::bail!( + "dylint-driver not found at {:?}.\n\ + Install it by running: cargo install cargo-dylint dylint-link\n\ + Then run: cargo +{} dylint --list", + dylint_driver, + toolchain + ); + } + + Ok(dylint_driver) + } + + /// Discover all lint libraries in the directory + fn discover_lint_libs(dir: &Path) -> Result> { + let mut libs = Vec::new(); + + for entry in std::fs::read_dir(dir) + .context(format!("Failed to read directory: {}", dir.display()))? + { + let entry = entry?; + let path = entry.path(); + + if path.is_file() { + let name = path.file_name().and_then(|n| n.to_str()).unwrap_or(""); + + // Check if it's a lint library (.dylib, .so, .dll) + if name.starts_with("lib") + && (name.ends_with(".dylib") || name.ends_with(".so") || name.ends_with(".dll")) + { + libs.push(path); + } + } + } + + Ok(libs) + } + + /// Get list of loaded lint names + pub fn loaded_lints(&self) -> Vec { + let lint_libs = self.lint_libs.lock().unwrap(); + lint_libs + .iter() + .filter_map(|p| { + p.file_stem() + .and_then(|s| s.to_str()) + .map(|s| s.to_string()) + }) + .collect() + } + + /// Check if dylint is available + pub fn is_available(&self) -> bool { + let lint_libs = self.lint_libs.lock().unwrap(); + !lint_libs.is_empty() + } +} diff --git a/language-server/src/lib.rs b/language-server/src/lib.rs index 1a67bee..ecbe22a 100644 --- a/language-server/src/lib.rs +++ b/language-server/src/lib.rs @@ -1,5 +1,7 @@ pub mod backend; pub mod core; +pub mod dylint_runner; pub mod server; pub use core::*; +pub use dylint_runner::*; diff --git a/language-server/src/main.rs b/language-server/src/main.rs index fb09a0d..e0596c9 100644 --- a/language-server/src/main.rs +++ b/language-server/src/main.rs @@ -1,5 +1,6 @@ mod backend; mod core; +mod dylint_runner; mod server; use log::debug;