From 5b86f64031e0e3ec3385feee3d8adf82df201178 Mon Sep 17 00:00:00 2001 From: Maksym Kupchenko Date: Sun, 11 Jan 2026 19:49:28 +0100 Subject: [PATCH 1/3] delte quickstart --- vsc-extension-quickstart.md | 44 ------------------------------------- 1 file changed, 44 deletions(-) delete mode 100644 vsc-extension-quickstart.md diff --git a/vsc-extension-quickstart.md b/vsc-extension-quickstart.md deleted file mode 100644 index c912064..0000000 --- a/vsc-extension-quickstart.md +++ /dev/null @@ -1,44 +0,0 @@ -# Welcome to your VS Code Extension - -## What's in the folder - -* This folder contains all of the files necessary for your extension. -* `package.json` - this is the manifest file in which you declare your extension and command. - * The sample plugin registers a command and defines its title and command name. With this information VS Code can show the command in the command palette. It doesn’t yet need to load the plugin. -* `src/extension.ts` - this is the main file where you will provide the implementation of your command. - * The file exports one function, `activate`, which is called the very first time your extension is activated (in this case by executing the command). Inside the `activate` function we call `registerCommand`. - * We pass the function containing the implementation of the command as the second parameter to `registerCommand`. - -## Get up and running straight away - -* Press `F5` to open a new window with your extension loaded. -* Run your command from the command palette by pressing (`Ctrl+Shift+P` or `Cmd+Shift+P` on Mac) and typing `Hello World`. -* Set breakpoints in your code inside `src/extension.ts` to debug your extension. -* Find output from your extension in the debug console. - -## Make changes - -* You can relaunch the extension from the debug toolbar after changing code in `src/extension.ts`. -* You can also reload (`Ctrl+R` or `Cmd+R` on Mac) the VS Code window with your extension to load your changes. - -## Explore the API - -* You can open the full set of our API when you open the file `node_modules/@types/vscode/index.d.ts`. - -## Run tests - -* Install the [Extension Test Runner](https://marketplace.visualstudio.com/items?itemName=ms-vscode.extension-test-runner) -* Run the "watch" task via the **Tasks: Run Task** command. Make sure this is running, or tests might not be discovered. -* Open the Testing view from the activity bar and click the Run Test" button, or use the hotkey `Ctrl/Cmd + ; A` -* See the output of the test result in the Test Results view. -* Make changes to `src/test/extension.test.ts` or create new test files inside the `test` folder. - * The provided test runner will only consider files matching the name pattern `**.test.ts`. - * You can create folders inside the `test` folder to structure your tests any way you want. - -## Go further - -* [Follow UX guidelines](https://code.visualstudio.com/api/ux-guidelines/overview) to create extensions that seamlessly integrate with VS Code's native interface and patterns. -* Reduce the extension size and improve the startup time by [bundling your extension](https://code.visualstudio.com/api/working-with-extensions/bundling-extension). -* [Publish your extension](https://code.visualstudio.com/api/working-with-extensions/publishing-extension) on the VS Code extension marketplace. -* Automate builds by setting up [Continuous Integration](https://code.visualstudio.com/api/working-with-extensions/continuous-integration). -* Integrate to the [report issue](https://code.visualstudio.com/api/get-started/wrapping-up#issue-reporting) flow to get issue and feature requests reported by users. From f5e9871c1c9e3d9188830dc9091dc93c45892c92 Mon Sep 17 00:00:00 2001 From: Maksym Kupchenko Date: Sun, 11 Jan 2026 19:51:40 +0100 Subject: [PATCH 2/3] delete unsafe math detector --- language-server/src/backend.rs | 1 - .../src/core/detectors/unsafe_math.rs | 150 ------ .../tests/unsafe_math_detector_test.rs | 457 ------------------ 3 files changed, 608 deletions(-) delete mode 100644 language-server/src/core/detectors/unsafe_math.rs delete mode 100644 language-server/tests/unsafe_math_detector_test.rs diff --git a/language-server/src/backend.rs b/language-server/src/backend.rs index ecbd839..2b89a13 100644 --- a/language-server/src/backend.rs +++ b/language-server/src/backend.rs @@ -894,7 +894,6 @@ 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(MissingSignerDetector::default()) // Ensure MissingSignerDetector is included .with_detector(ManualLamportsZeroingDetector::default()) .with_detector(SysvarAccountDetector::default()) diff --git a/language-server/src/core/detectors/unsafe_math.rs b/language-server/src/core/detectors/unsafe_math.rs deleted file mode 100644 index cfec123..0000000 --- a/language-server/src/core/detectors/unsafe_math.rs +++ /dev/null @@ -1,150 +0,0 @@ -use super::detector::Detector; -use super::detector_config::DetectorConfig; -use crate::core::utilities::DiagnosticBuilder; -use std::path::PathBuf; -use syn::spanned::Spanned; -use syn::{BinOp, parse_str, visit::Visit}; -use tower_lsp::lsp_types::{Diagnostic, DiagnosticSeverity}; - -#[derive(Default)] -pub struct UnsafeMathDetector { - diagnostics: Vec, - config: DetectorConfig, -} - -impl UnsafeMathDetector { - #[allow(dead_code)] - pub fn with_config(config: DetectorConfig) -> Self { - Self { - diagnostics: Vec::new(), - config, - } - } - - /// Check if an expression has explicit type casting or annotation - fn has_explicit_type(&self, expr: &syn::Expr) -> bool { - match expr { - // Type casting: expr as Type - syn::Expr::Cast(_) => true, - // Literal with type suffix: 42u64, 3.14f32 - syn::Expr::Lit(lit_expr) => { - if let syn::Lit::Int(int_lit) = &lit_expr.lit { - !int_lit.suffix().is_empty() - } else if let syn::Lit::Float(float_lit) = &lit_expr.lit { - !float_lit.suffix().is_empty() - } else { - false - } - } - // Method call with explicit turbo fish: value.into::() - syn::Expr::MethodCall(method_call) => method_call.turbofish.is_some(), - // Function call with explicit generics: from::(value) - syn::Expr::Call(call_expr) => { - if let syn::Expr::Path(path_expr) = &*call_expr.func { - path_expr - .path - .segments - .iter() - .any(|seg| seg.arguments != syn::PathArguments::None) - } else { - false - } - } - _ => false, - } - } - - /// Check if an expression is a safe literal (small integer) - fn is_safe_literal(&self, expr: &syn::Expr) -> bool { - if let syn::Expr::Lit(lit_expr) = expr { - if let syn::Lit::Int(int_lit) = &lit_expr.lit { - // Consider small integers safe (less than 2^32) - if let Ok(value) = int_lit.base10_parse::() { - return value < (1u64 << 32); - } - } - } - false - } - - /// Check operand types for safety - fn check_operand_types(&self, left: &syn::Expr, right: &syn::Expr) -> bool { - let left_explicit = self.has_explicit_type(left); - let right_explicit = self.has_explicit_type(right); - let left_safe_literal = self.is_safe_literal(left); - let right_safe_literal = self.is_safe_literal(right); - - left_explicit || right_explicit || (left_safe_literal && right_safe_literal) - } -} - -impl Detector for UnsafeMathDetector { - fn id(&self) -> &'static str { - "UNSAFE_ARITHMETIC" - } - - fn name(&self) -> &'static str { - "Unsafe Math Operations" - } - - fn description(&self) -> &'static str { - "Detects unchecked arithmetic operations that could lead to overflow/underflow vulnerabilities" - } - - fn message(&self) -> &'static str { - "Unchecked arithmetic operation detected. Consider using checked_add(), checked_sub(), checked_mul(), or checked_div() to prevent overflow/underflow." - } - - fn default_severity(&self) -> DiagnosticSeverity { - DiagnosticSeverity::ERROR - } - - fn analyze(&mut self, content: &str, _file_path: Option<&PathBuf>) -> Vec { - self.diagnostics.clear(); - - // Run default detection logic - if let Ok(syntax_tree) = parse_str::(content) { - self.visit_file(&syntax_tree); - } - - self.diagnostics.clone() - } -} - -impl<'ast> Visit<'ast> for UnsafeMathDetector { - fn visit_expr_binary(&mut self, node: &'ast syn::ExprBinary) { - match node.op { - BinOp::Add(_) - | BinOp::Sub(_) - | BinOp::Mul(_) - | BinOp::Div(_) - | BinOp::AddAssign(_) - | BinOp::SubAssign(_) - | BinOp::MulAssign(_) - | BinOp::DivAssign(_) => { - // Check if operands have explicit type annotations or are literals - let is_type_safe = self.check_operand_types(&node.left, &node.right); - - // Only flag if we can't determine safe types - if !is_type_safe { - let severity = self - .config - .severity_override - .unwrap_or(self.default_severity()); - - self.diagnostics.push(DiagnosticBuilder::create( - DiagnosticBuilder::create_range_from_span(node.span()), - self.message().to_string(), - severity, - self.id().to_string(), - None, - )); - } - } - _ => {} - } - - // Continue visiting children - syn::visit::visit_expr_binary(self, node); - } -} diff --git a/language-server/tests/unsafe_math_detector_test.rs b/language-server/tests/unsafe_math_detector_test.rs deleted file mode 100644 index 9eb1344..0000000 --- a/language-server/tests/unsafe_math_detector_test.rs +++ /dev/null @@ -1,457 +0,0 @@ -use language_server::core::detectors::{detector::Detector, unsafe_math::UnsafeMathDetector}; -use tower_lsp::lsp_types::DiagnosticSeverity; - -#[test] -fn test_detector_metadata() { - let detector = UnsafeMathDetector::default(); - - assert_eq!(detector.id(), "UNSAFE_ARITHMETIC"); - assert_eq!(detector.name(), "Unsafe Math Operations"); - assert_eq!( - detector.description(), - "Detects unchecked arithmetic operations that could lead to overflow/underflow vulnerabilities" - ); - assert_eq!(detector.default_severity(), DiagnosticSeverity::ERROR); -} - -#[test] -fn test_detects_addition_in_instruction() { - let mut detector = UnsafeMathDetector::default(); - - let code_with_addition = r#" - use anchor_lang::prelude::*; - - #[program] - pub mod vault_program { - use super::*; - - pub fn deposit(ctx: Context, amount: u64) -> Result<()> { - ctx.accounts.vault.balance = ctx.accounts.vault.balance + amount; - Ok(()) - } - } - - #[derive(Accounts)] - pub struct Deposit<'info> { - #[account(mut)] - pub vault: Account<'info, Vault>, - pub user: Signer<'info>, - } - - #[account] - pub struct Vault { - pub balance: u64, - pub authority: Pubkey, - } - "#; - - let diagnostics = detector.analyze(code_with_addition, None); - assert_eq!(diagnostics.len(), 1); - - let diagnostic = &diagnostics[0]; - assert_eq!(diagnostic.severity, Some(DiagnosticSeverity::ERROR)); - assert!( - diagnostic - .message - .contains("Unchecked arithmetic operation detected") - ); - assert!(diagnostic.message.contains("checked_add()")); -} - -#[test] -fn test_detects_multiple_arithmetic_operations() { - let mut detector = UnsafeMathDetector::default(); - - let code_with_multiple_operations = r#" - use anchor_lang::prelude::*; - - #[program] - pub mod defi_program { - use super::*; - - pub fn calculate_rewards(ctx: Context, stake_amount: u64, duration: u64) -> Result<()> { - let base_reward = stake_amount + ctx.accounts.pool.base_rate; - let time_bonus = duration - ctx.accounts.pool.time_multiplier; - let total_reward = base_reward * time_bonus; - - ctx.accounts.user.rewards = total_reward; - Ok(()) - } - } - - #[derive(Accounts)] - pub struct CalculateRewards<'info> { - #[account(mut)] - pub user: Account<'info, User>, - pub pool: Account<'info, Pool>, - } - - #[account] - pub struct User { - pub rewards: u64, - pub stake_amount: u64, - } - - #[account] - pub struct Pool { - pub base_rate: u64, - pub time_multiplier: u64, - } - "#; - - let diagnostics = detector.analyze(code_with_multiple_operations, None); - assert_eq!(diagnostics.len(), 3); - - for diagnostic in &diagnostics { - assert_eq!(diagnostic.severity, Some(DiagnosticSeverity::ERROR)); - assert!( - diagnostic - .message - .contains("Unchecked arithmetic operation detected") - ); - } -} - -#[test] -fn test_nested_addition_in_anchor_context() { - let mut detector = UnsafeMathDetector::default(); - - let code_with_nested_additions = r#" - use anchor_lang::prelude::*; - - #[program] - pub mod complex_math { - use super::*; - - pub fn complex_calculation(ctx: Context, a: u64, b: u64, c: u64, d: u64) -> Result<()> { - let result = (a + b) + (c + d); - ctx.accounts.storage.value = result; - Ok(()) - } - } - - #[derive(Accounts)] - pub struct ComplexCalc<'info> { - #[account(mut)] - pub storage: Account<'info, Storage>, - } - - #[account] - pub struct Storage { - pub value: u64, - } - "#; - - let diagnostics = detector.analyze(code_with_nested_additions, None); - // Should detect 3 additions: a+b, c+d, and (a+b)+(c+d) - assert_eq!(diagnostics.len(), 3); -} - -#[test] -fn test_no_detection_without_addition() { - let mut detector = UnsafeMathDetector::default(); - - let code_without_addition = r#" - use anchor_lang::prelude::*; - - #[program] - pub mod safe_program { - use super::*; - - pub fn safe_operation(ctx: Context) -> Result<()> { - ctx.accounts.user.last_update = Clock::get()?.unix_timestamp; - ctx.accounts.user.status = UserStatus::Active; - Ok(()) - } - } - - #[derive(Accounts)] - pub struct SafeOp<'info> { - #[account(mut)] - pub user: Account<'info, User>, - } - - #[account] - pub struct User { - pub last_update: i64, - pub status: UserStatus, - } - - #[derive(AnchorSerialize, AnchorDeserialize, Clone, PartialEq, Eq)] - pub enum UserStatus { - Active, - Inactive, - } - "#; - - let diagnostics = detector.analyze(code_without_addition, None); - assert_eq!(diagnostics.len(), 0); -} - -#[test] -fn test_invalid_syntax_handling() { - let mut detector = UnsafeMathDetector::default(); - - let invalid_code = r#" - use anchor_lang::prelude::*; - - #[program] - pub mod broken_program { - use super::*; - - pub fn broken_function(ctx: Context { - let result = a + ; - } - } - "#; - - // Should handle invalid syntax gracefully and return empty diagnostics - let diagnostics = detector.analyze(invalid_code, None); - assert_eq!(diagnostics.len(), 0); -} - -#[test] -fn test_addition_in_different_anchor_contexts() { - let mut detector = UnsafeMathDetector::default(); - - let code_with_various_contexts = r#" - use anchor_lang::prelude::*; - - #[program] - pub mod multi_context_program { - use super::*; - - pub fn instruction_handler(ctx: Context, amount: u64) -> Result<()> { - ctx.accounts.account.balance = ctx.accounts.account.balance + amount; - Ok(()) - } - } - - impl<'info> MyContext<'info> { - fn helper_method(&self, value: u64) -> u64 { - let result = self.account.balance + value; - result - } - } - - #[derive(Accounts)] - pub struct MyContext<'info> { - #[account(mut)] - pub account: Account<'info, MyAccount>, - } - - #[account] - pub struct MyAccount { - pub balance: u64, - } - - mod utils { - pub fn calculate_fee(base: u64, rate: u64) -> u64 { - base + rate - } - } - "#; - - let diagnostics = detector.analyze(code_with_various_contexts, None); - assert_eq!(diagnostics.len(), 3); -} - -#[test] -fn test_token_transfer_with_unsafe_math() { - let mut detector = UnsafeMathDetector::default(); - - let token_program_code = r#" - use anchor_lang::prelude::*; - use anchor_spl::token::{self, Token, TokenAccount, Transfer}; - - #[program] - pub mod token_vault { - use super::*; - - pub fn deposit_tokens(ctx: Context, amount: u64) -> Result<()> { - // Unsafe: could overflow - ctx.accounts.vault.total_deposits = ctx.accounts.vault.total_deposits + amount; - - // Safe: explicit type annotation - let a: u8 = 8 + 12; - - let cpi_accounts = Transfer { - from: ctx.accounts.user_token_account.to_account_info(), - to: ctx.accounts.vault_token_account.to_account_info(), - authority: ctx.accounts.user.to_account_info(), - }; - - let cpi_program = ctx.accounts.token_program.to_account_info(); - let cpi_ctx = CpiContext::new(cpi_program, cpi_accounts); - - token::transfer(cpi_ctx, amount)?; - Ok(()) - } - } - - #[derive(Accounts)] - pub struct DepositTokens<'info> { - #[account(mut)] - pub vault: Account<'info, Vault>, - #[account(mut)] - pub vault_token_account: Account<'info, TokenAccount>, - #[account(mut)] - pub user_token_account: Account<'info, TokenAccount>, - pub user: Signer<'info>, - pub token_program: Program<'info, Token>, - } - - #[account] - pub struct Vault { - pub total_deposits: u64, - pub authority: Pubkey, - } - "#; - - let diagnostics = detector.analyze(token_program_code, None); - // Should detect the unsafe addition in total_deposits calculation - assert_eq!(diagnostics.len(), 1); -} - -#[test] -fn test_detector_state_isolation() { - let mut detector1 = UnsafeMathDetector::default(); - let mut detector2 = UnsafeMathDetector::default(); - - let code = r#" - use anchor_lang::prelude::*; - - #[program] - pub mod test_program { - use super::*; - - pub fn test_instruction(ctx: Context, amount: u64) -> Result<()> { - let result = ctx.accounts.account.value + amount; - Ok(()) - } - } - - #[derive(Accounts)] - pub struct TestCtx<'info> { - #[account(mut)] - pub account: Account<'info, TestAccount>, - } - - #[account] - pub struct TestAccount { - pub value: u64, - } - "#; - - let diagnostics1 = detector1.analyze(code, None); - let diagnostics2 = detector2.analyze(code, None); - - // Each detector instance should produce the same results - assert_eq!(diagnostics1.len(), diagnostics2.len()); - assert_eq!(diagnostics1.len(), 1); -} - -#[test] -fn test_detects_assign_addition_in_instruction() { - let mut detector = UnsafeMathDetector::default(); - - let code_with_assign_addition = r#" - use anchor_lang::prelude::*; - - #[program] - pub mod vault_program { - use super::*; - - pub fn deposit(ctx: Context, amount: u64) -> Result<()> { - ctx.accounts.vault.balance += amount; - Ok(()) - } - } - - #[derive(Accounts)] - pub struct Deposit<'info> { - #[account(mut)] - pub vault: Account<'info, Vault>, - pub user: Signer<'info>, - } - - #[account] - pub struct Vault { - pub balance: u64, - pub authority: Pubkey, - } - "#; - - let diagnostics = detector.analyze(code_with_assign_addition, None); - assert_eq!(diagnostics.len(), 1); - - let diagnostic = &diagnostics[0]; - assert_eq!(diagnostic.severity, Some(DiagnosticSeverity::ERROR)); - assert!( - diagnostic - .message - .contains("Unchecked arithmetic operation detected") - ); - assert!(diagnostic.message.contains("checked_add()")); -} - -#[test] -fn test_detects_multiple_assign_arithmetic_operations_including_division() { - let mut detector = UnsafeMathDetector::default(); - - let code_with_assign_operations = r#" - use anchor_lang::prelude::*; - - #[program] - pub mod defi_program { - use super::*; - - pub fn calculate_rewards(ctx: Context, stake_amount: u64, duration: u64) -> Result<()> { - let mut base_reward = stake_amount; - base_reward += ctx.accounts.pool.base_rate; - - let mut time_bonus = duration; - time_bonus -= ctx.accounts.pool.time_multiplier; - - let mut total_reward = base_reward; - total_reward *= time_bonus; - total_reward /= ctx.accounts.pool.divisor; - - ctx.accounts.user.rewards = total_reward; - Ok(()) - } - } - - #[derive(Accounts)] - pub struct CalculateRewards<'info> { - #[account(mut)] - pub user: Account<'info, User>, - pub pool: Account<'info, Pool>, - } - - #[account] - pub struct User { - pub rewards: u64, - pub stake_amount: u64, - } - - #[account] - pub struct Pool { - pub base_rate: u64, - pub time_multiplier: u64, - pub divisor: u64, - } - "#; - - let diagnostics = detector.analyze(code_with_assign_operations, None); - assert_eq!(diagnostics.len(), 4); - - for diagnostic in &diagnostics { - assert_eq!(diagnostic.severity, Some(DiagnosticSeverity::ERROR)); - assert!( - diagnostic - .message - .contains("Unchecked arithmetic operation detected") - ); - } -} From 33450443eb8e8ac4025be8c91d2723670abc534f Mon Sep 17 00:00:00 2001 From: Maksym Kupchenko Date: Sun, 11 Jan 2026 20:45:35 +0100 Subject: [PATCH 3/3] add missing signer detector --- beta-tester-guide.md | 46 -- extension/detectors/missing_signer/.gitignore | 2 + extension/detectors/missing_signer/Cargo.toml | 17 + extension/detectors/missing_signer/README.md | 51 ++ .../detectors/missing_signer/rust-toolchain | 3 + extension/detectors/missing_signer/src/lib.rs | 253 +++++++++ extension/detectors/missing_signer/ui/main.rs | 6 + language-server/src/backend.rs | 5 +- .../src/core/detectors/missing_signer.rs | 478 ------------------ language-server/src/core/detectors/mod.rs | 4 - 10 files changed, 334 insertions(+), 531 deletions(-) delete mode 100644 beta-tester-guide.md create mode 100644 extension/detectors/missing_signer/.gitignore create mode 100644 extension/detectors/missing_signer/Cargo.toml create mode 100644 extension/detectors/missing_signer/README.md create mode 100644 extension/detectors/missing_signer/rust-toolchain create mode 100644 extension/detectors/missing_signer/src/lib.rs create mode 100644 extension/detectors/missing_signer/ui/main.rs delete mode 100644 language-server/src/core/detectors/missing_signer.rs diff --git a/beta-tester-guide.md b/beta-tester-guide.md deleted file mode 100644 index bb49074..0000000 --- a/beta-tester-guide.md +++ /dev/null @@ -1,46 +0,0 @@ -# Beta Tester Guide for Solana VSCode Extension - -Thank you for participating in the beta testing of our Solana VSCode Extension! Your feedback is invaluable in helping us improve the extension before its official release. - -Make sure you are testing the `pre-release` version. - -## What to Focus On - -### 1. Core Functionality - -- **Solana Program Detection**: Does the extension correctly identify Solana programs in your workspace? -- **Code Analysis**: Are the detectors finding real issues in your code? Are there false positives? -- **Performance**: Does the extension remain responsive with large programs? - -### 2. Key Features to Test - -- **Security Detectors**: Test all security detectors with both valid and invalid code patterns -- **Code Coverage**: Verify that coverage reporting works correctly with your test suites -- **Integration with Solana CLI**: Check if the extension works well with your installed Solana tools - -### 3. User Experience - -- **UI Elements**: Are notifications, highlights, and other visual elements clear and helpful? -- **Settings**: Do the configuration options work as expected? -- **Documentation**: Is the documentation clear and sufficient for your needs? - -## How to Submit Issues - -When you encounter a problem or have a suggestion: - -1. **Check Existing Issues**: Before submitting, please check if the issue has already been reported. - -2. **Fill Out the Issue Template**: Complete all relevant sections of the issue template to help us understand and reproduce the issue. - -3. **Be Specific**: Include exact steps to reproduce, error messages, and screenshots when possible. - -4. **Include Environment Details**: Your OS, VS Code version, and extension version are crucial for debugging. - -## Feedback Process - -1. We review all issues -2. Issues may be labeled for further clarification, prioritization, or as duplicates -3. You may be asked for additional information if needed -4. Once addressed, you'll be notified on the issue thread - -Thank you for helping us build a better Solana development experience! diff --git a/extension/detectors/missing_signer/.gitignore b/extension/detectors/missing_signer/.gitignore new file mode 100644 index 0000000..2c96eb1 --- /dev/null +++ b/extension/detectors/missing_signer/.gitignore @@ -0,0 +1,2 @@ +target/ +Cargo.lock diff --git a/extension/detectors/missing_signer/Cargo.toml b/extension/detectors/missing_signer/Cargo.toml new file mode 100644 index 0000000..e325c77 --- /dev/null +++ b/extension/detectors/missing_signer/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "missing_signer" +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/missing_signer/README.md b/extension/detectors/missing_signer/README.md new file mode 100644 index 0000000..82a4309 --- /dev/null +++ b/extension/detectors/missing_signer/README.md @@ -0,0 +1,51 @@ +# missing_signer + +### What it does + +Detects Anchor program instructions that have no signer accounts, which could allow unauthorized access. + +### Why is this bad? + +In Solana programs, missing signer checks can lead to serious security vulnerabilities: + +- Anyone can call the instruction without authorization +- Unauthorized users can modify program state +- Attackers can drain funds or manipulate data +- No way to verify who initiated the transaction + +### Example + +```rust +// Bad - No signer field +#[derive(Accounts)] +pub struct Transfer<'info> { + #[account(mut)] + pub from: Account<'info, TokenAccount>, + #[account(mut)] + pub to: Account<'info, TokenAccount>, +} + +pub fn transfer(ctx: Context, amount: u64) -> Result<()> { + // No signer - anyone can transfer from any account! + Ok(()) +} +``` + +Use instead: + +```rust +// Good - Has signer field +#[derive(Accounts)] +pub struct Transfer<'info> { + pub authority: Signer<'info>, + #[account(mut)] + pub from: Account<'info, TokenAccount>, + #[account(mut)] + pub to: Account<'info, TokenAccount>, +} + +pub fn transfer(ctx: Context, amount: u64) -> Result<()> { + // authority.key() can be used to verify ownership + Ok(()) +} +``` diff --git a/extension/detectors/missing_signer/rust-toolchain b/extension/detectors/missing_signer/rust-toolchain new file mode 100644 index 0000000..b8e5648 --- /dev/null +++ b/extension/detectors/missing_signer/rust-toolchain @@ -0,0 +1,3 @@ +[toolchain] +channel = "nightly-2025-09-18" +components = ["llvm-tools-preview", "rustc-dev"] diff --git a/extension/detectors/missing_signer/src/lib.rs b/extension/detectors/missing_signer/src/lib.rs new file mode 100644 index 0000000..349faca --- /dev/null +++ b/extension/detectors/missing_signer/src/lib.rs @@ -0,0 +1,253 @@ +#![feature(rustc_private)] +#![warn(unused_extern_crates)] + +extern crate rustc_hir; +extern crate rustc_middle; +extern crate rustc_span; + +use rustc_hir::def::Res; +use rustc_hir::def_id::DefId; +use rustc_hir::{FnDecl, FnSig, GenericArg, Item, ItemKind, QPath, Ty, TyKind}; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_span::symbol::sym; +use std::collections::HashMap; + +dylint_linting::declare_late_lint! { + /// ### What it does + /// Detects Anchor program instructions that have no signer accounts, which could allow + /// unauthorized access. + /// + /// ### Why is this bad? + /// In Solana programs, missing signer checks can lead to serious security vulnerabilities: + /// - Anyone can call the instruction without authorization + /// - Unauthorized users can modify program state + /// - Attackers can drain funds or manipulate data + /// - No way to verify who initiated the transaction + /// + /// ### Example + /// + /// Bad: + /// ```rust + /// #[derive(Accounts)] + /// pub struct Transfer<'info> { + /// #[account(mut)] + /// pub from: Account<'info, TokenAccount>, + /// #[account(mut)] + /// pub to: Account<'info, TokenAccount>, + /// } + /// + /// pub fn transfer(ctx: Context, amount: u64) -> Result<()> { + /// // No signer - anyone can transfer from any account! + /// Ok(()) + /// } + /// ``` + /// + /// Good: + /// ```rust + /// #[derive(Accounts)] + /// pub struct Transfer<'info> { + /// pub authority: Signer<'info>, + /// #[account(mut)] + /// pub from: Account<'info, TokenAccount>, + /// #[account(mut)] + /// pub to: Account<'info, TokenAccount>, + /// } + /// + /// pub fn transfer(ctx: Context, amount: u64) -> Result<()> { + /// // authority.key() can be used to verify ownership + /// Ok(()) + /// } + /// ``` + pub MISSING_SIGNER, + Warn, + "detects Anchor program instructions with no signer accounts" +} + +/// Stores information about Accounts structs and whether they have signers +struct AccountsAnalyzer { + /// Maps struct DefId to whether it has a signer field + has_signer: HashMap, +} + +impl AccountsAnalyzer { + fn new() -> Self { + Self { + has_signer: HashMap::new(), + } + } + + /// Check if a struct has a Signer field + fn check_struct_has_signer(&mut self, cx: &LateContext<'_>, def_id: DefId) -> bool { + // Check cache first + if let Some(&cached) = self.has_signer.get(&def_id) { + return cached; + } + + let tcx = cx.tcx; + let adt_def = tcx.adt_def(def_id); + + // Check all fields in the struct + for variant in adt_def.variants() { + for field in &variant.fields { + let field_ty = tcx.type_of(field.did).instantiate_identity(); + + // Check if this field is a Signer<'info> + if self.is_signer_type(cx, field_ty) { + self.has_signer.insert(def_id, true); + return true; + } + + // Check if this field is another Accounts struct (nested) + if let Some(nested_def_id) = self.get_struct_def_id(field_ty) { + // Only check if it's an Accounts struct to avoid checking random nested types + if self.has_accounts_derive(cx, nested_def_id) { + // Recursively check the nested Accounts struct + if self.check_struct_has_signer(cx, nested_def_id) { + self.has_signer.insert(def_id, true); + return true; + } + } + } + } + } + + self.has_signer.insert(def_id, false); + false + } + + /// Check if a type is Signer<'info> + fn is_signer_type(&self, cx: &LateContext<'_>, ty: rustc_middle::ty::Ty<'_>) -> bool { + if let rustc_middle::ty::TyKind::Adt(adt_def, _substs) = ty.kind() { + let type_name = cx.tcx.item_name(adt_def.did()); + return type_name.as_str() == "Signer"; + } + false + } + + /// Get the DefId of a struct type + fn get_struct_def_id(&self, ty: rustc_middle::ty::Ty<'_>) -> Option { + if let rustc_middle::ty::TyKind::Adt(adt_def, _) = ty.kind() { + if adt_def.is_struct() { + return Some(adt_def.did()); + } + } + None + } + + /// Check if a struct has #[derive(Accounts)] + fn has_accounts_derive(&self, cx: &LateContext<'_>, def_id: DefId) -> bool { + let tcx = cx.tcx; + let attrs = tcx.get_attrs(def_id, sym::derive); + + for attr in attrs { + // Check if it derives Accounts + let attr_str = format!("{:?}", attr); + if attr_str.contains("Accounts") { + return true; + } + } + false + } +} + +impl<'tcx> LateLintPass<'tcx> for MissingSigner { + fn check_item(&mut self, cx: &LateContext<'tcx>, item: &'tcx Item<'tcx>) { + // Only check public functions that take Context parameter + // We'll check the struct T when we find it's used in Context + if let ItemKind::Fn { sig, .. } = item.kind { + let def_id = item.owner_id.to_def_id(); + let visibility = cx.tcx.visibility(def_id); + if visibility.is_public() { + self.check_program_function(cx, item, &sig); + } + } + } +} + +impl MissingSigner { + /// Check a program function for missing signer + fn check_program_function(&mut self, cx: &LateContext<'_>, item: &Item<'_>, sig: &FnSig<'_>) { + // Look for Context parameter + if let Some(context_type_def_id) = self.extract_context_type(cx, sig.decl) { + let mut analyzer = AccountsAnalyzer::new(); + + // Check if the Accounts struct has a signer + if !analyzer.check_struct_has_signer(cx, context_type_def_id) { + // Report on the function (instruction level) - use ident span for just the function name + let fn_span = sig.span; + clippy_utils::diagnostics::span_lint_and_help( + cx, + MISSING_SIGNER, + fn_span, + "Instruction has no signer account", + None, + "Consider adding a Signer<'info> field to the accounts struct to ensure proper authorization", + ); + + // Also report on the struct definition itself + if let Some(struct_span) = self.get_struct_span(cx, context_type_def_id) { + clippy_utils::diagnostics::span_lint_and_help( + cx, + MISSING_SIGNER, + struct_span, + "Accounts struct has no signer field", + None, + "Consider adding a Signer<'info> field to ensure proper authorization", + ); + } + } + } + } + + /// Get the span of a struct definition from its DefId + fn get_struct_span(&self, cx: &LateContext<'_>, def_id: DefId) -> Option { + // Only works for local definitions (not external crates) + if let Some(local_def_id) = def_id.as_local() { + // Get the full definition span (entire struct declaration line including braces) + return Some(cx.tcx.def_span(local_def_id)); + } + None + } + + /// Extract the Context type from function parameters + fn extract_context_type(&self, cx: &LateContext<'_>, decl: &FnDecl<'_>) -> Option { + for param in decl.inputs { + if let Some(def_id) = self.get_context_generic_type(cx, param) { + return Some(def_id); + } + } + None + } + + /// Get the generic type T from Context + fn get_context_generic_type(&self, cx: &LateContext<'_>, ty: &Ty<'_>) -> Option { + if let TyKind::Path(QPath::Resolved(None, path)) = &ty.kind { + // Check if this is a Context type + if let Res::Def(_, def_id) = path.res { + let type_name = cx.tcx.item_name(def_id); + if type_name.as_str() != "Context" { + return None; + } + + // Extract the generic argument (the Accounts struct) + if let Some(segment) = path.segments.last() { + if let Some(args) = segment.args { + for arg in args.args { + if let GenericArg::Type(inner_ty) = arg { + // Get the DefId of the inner type + if let TyKind::Path(QPath::Resolved(None, inner_path)) = + &inner_ty.kind + { + if let Res::Def(_, inner_def_id) = inner_path.res { + return Some(inner_def_id); + } + } + } + } + } + } + } + } + None + } +} diff --git a/extension/detectors/missing_signer/ui/main.rs b/extension/detectors/missing_signer/ui/main.rs new file mode 100644 index 0000000..ed315d0 --- /dev/null +++ b/extension/detectors/missing_signer/ui/main.rs @@ -0,0 +1,6 @@ +// Note: Full testing requires an Anchor project environment so no tests are included +// This lint is designed to work with compiled Anchor programs + +fn main() { + println!("missing_signer lint loaded successfully"); +} diff --git a/language-server/src/backend.rs b/language-server/src/backend.rs index 2b89a13..65d8756 100644 --- a/language-server/src/backend.rs +++ b/language-server/src/backend.rs @@ -4,8 +4,8 @@ use crate::core::{ DetectorStatusNotification, DylintDetectorManager, FileScanner, ImmutableAccountMutatedDetector, InstructionAttributeInvalidDetector, InstructionAttributeUnusedDetector, ManualLamportsZeroingDetector, MissingCheckCommentDetector, - MissingInitspaceDetector, MissingSignerDetector, ScanCompleteNotification, ScanResult, - ScanSummary, SysvarAccountDetector, + MissingInitspaceDetector, ScanCompleteNotification, ScanResult, ScanSummary, + SysvarAccountDetector, }; use crate::dylint_runner::DylintRunner; use log::{info, warn}; @@ -894,7 +894,6 @@ pub struct DetectorStats { fn create_default_registry() -> DetectorRegistry { info!("Creating new detector registry with all detectors"); let registry = DetectorRegistryBuilder::new() - .with_detector(MissingSignerDetector::default()) // Ensure MissingSignerDetector is included .with_detector(ManualLamportsZeroingDetector::default()) .with_detector(SysvarAccountDetector::default()) .with_detector(ImmutableAccountMutatedDetector::default()) diff --git a/language-server/src/core/detectors/missing_signer.rs b/language-server/src/core/detectors/missing_signer.rs deleted file mode 100644 index 5aa5bc0..0000000 --- a/language-server/src/core/detectors/missing_signer.rs +++ /dev/null @@ -1,478 +0,0 @@ -use super::detector::Detector; -use super::detector_config::DetectorConfig; -use crate::core::utilities::{DiagnosticBuilder, anchor_patterns::AnchorPatterns}; -use log::{debug, info, warn}; -use std::collections::{HashMap, HashSet}; -use std::fs; -use std::path::{Path, PathBuf}; -use syn::spanned::Spanned; -use syn::{parse_str, visit::Visit}; -use tower_lsp::lsp_types::{Diagnostic, DiagnosticSeverity, Range}; - -#[derive(Default)] -pub struct MissingSignerDetector { - diagnostics: Vec, - config: DetectorConfig, - // Store context name and its location - contexts: HashMap, - // Track which contexts have signer fields - context_has_signer: HashMap, - // Track composite contexts and their components - composite_contexts: HashMap>, - // Track which contexts we've already processed to avoid cycles - processed_contexts: HashSet, - file_path: Option, - workspace_root: Option, -} - -impl MissingSignerDetector { - #[allow(dead_code)] - pub fn with_config(config: DetectorConfig) -> Self { - Self { - diagnostics: Vec::new(), - config, - contexts: HashMap::new(), - context_has_signer: HashMap::new(), - composite_contexts: HashMap::new(), - processed_contexts: HashSet::new(), - file_path: None, - workspace_root: None, - } - } - - /// Check if a field is a Signer type - fn is_signer_field(&self, field: &syn::Field) -> bool { - AnchorPatterns::is_signer_field(field) - } - - /// Check if a field is a composite account type (another Accounts struct) - fn is_composite_field(&self, field: &syn::Field) -> Option { - if let syn::Type::Path(type_path) = &field.ty { - if let Some(segment) = type_path.path.segments.last() { - // Skip known Anchor types that aren't composite - let name = segment.ident.to_string(); - if !matches!( - name.as_str(), - "Account" - | "AccountInfo" - | "UncheckedAccount" - | "Signer" - | "Program" - | "SystemProgram" - | "Clock" - | "Rent" - | "TokenAccount" - ) { - return Some(name); - } - } - } - None - } - - /// Check if an accounts struct has any signer fields, including nested ones - fn check_accounts_struct(&mut self, struct_name: &str, item_struct: &syn::ItemStruct) -> bool { - // Avoid infinite recursion by tracking processed contexts - if self.processed_contexts.contains(struct_name) { - // Return cached result if available - return self - .context_has_signer - .get(struct_name) - .copied() - .unwrap_or(false); - } - - // Mark as being processed - self.processed_contexts.insert(struct_name.to_string()); - - let mut has_direct_signer = false; - let mut composite_fields = Vec::new(); - - if let syn::Fields::Named(fields) = &item_struct.fields { - for field in &fields.named { - // Check for direct signer fields - if self.is_signer_field(field) { - has_direct_signer = true; - break; - } - - // Check for composite fields - if let Some(composite_type) = self.is_composite_field(field) { - composite_fields.push(composite_type); - } - } - } - - // If we found a direct signer, we're done - if has_direct_signer { - self.context_has_signer - .insert(struct_name.to_string(), true); - return true; - } - - // Store composite relationships for later analysis - if !composite_fields.is_empty() { - self.composite_contexts - .insert(struct_name.to_string(), composite_fields.clone()); - - // Check if any of the composite fields have a signer (recursively if needed) - if self.check_composite_fields_for_signer(&composite_fields) { - self.context_has_signer - .insert(struct_name.to_string(), true); - return true; - } - } - - // No direct or composite signer found - self.context_has_signer - .insert(struct_name.to_string(), false); - false - } - - /// Check if any of the composite fields have a signer (recursively if needed) - fn check_composite_fields_for_signer(&mut self, composite_fields: &[String]) -> bool { - for composite_type in composite_fields { - // Check if we already know the result - if let Some(has_signer) = self.context_has_signer.get(composite_type) { - if *has_signer { - return true; - } - } else { - // We don't know yet, so we need to find and check the struct - // This will happen during the search_for_context_definitions phase - // when we scan other files - } - - // Check if this composite field itself has composite fields - // Clone the nested fields to avoid borrowing issues - let nested_fields_option = self.composite_contexts.get(composite_type).cloned(); - if let Some(nested_fields) = nested_fields_option { - // Avoid cycles by checking if we've already processed this context - if !self.processed_contexts.contains(composite_type) { - self.processed_contexts.insert(composite_type.clone()); - - // Recursively check nested composite fields - if self.check_composite_fields_for_signer(&nested_fields) { - self.context_has_signer.insert(composite_type.clone(), true); - return true; - } - } - } - } - - false - } - - /// Search for context definitions in other files - fn search_for_context_definitions(&mut self) { - // Skip if we don't have a workspace root - let Some(workspace_root) = &self.workspace_root else { - warn!("No workspace root set, skipping context search"); - return; - }; - - // Skip if we don't have any contexts to search for - if self.contexts.is_empty() { - return; - } - - // Get all contexts that we need to check, including composite components - let mut contexts_to_find = HashSet::new(); - - // Add direct contexts - for ctx in self.contexts.keys() { - contexts_to_find.insert(ctx.clone()); - } - - // Add composite components that we don't have info about yet - for components in self.composite_contexts.values() { - for component in components { - if !self.context_has_signer.contains_key(component) { - contexts_to_find.insert(component.clone()); - } - } - } - - // Filter to only contexts we haven't found yet - let missing_contexts: Vec = contexts_to_find - .into_iter() - .filter(|ctx| !self.context_has_signer.contains_key(ctx)) - .collect(); - - if missing_contexts.is_empty() { - return; - } - - info!("Searching for context definitions: {:?}", missing_contexts); - - // Walk through Rust files in the workspace - if let Ok(rust_files) = self.walk_directory(workspace_root, &["rs"]) { - for file_path in rust_files { - // Skip the current file - we've already processed it - if let Some(current_file) = &self.file_path { - if current_file == &file_path { - continue; - } - } - - // Read and parse the file - if let Ok(content) = fs::read_to_string(&file_path) { - if let Ok(syntax_tree) = parse_str::(&content) { - // Look for account structs that match our missing contexts - for item in &syntax_tree.items { - if let syn::Item::Struct(item_struct) = item { - if AnchorPatterns::is_accounts_struct(item_struct) { - let struct_name = item_struct.ident.to_string(); - - // Check if this is one of our missing contexts - if missing_contexts.contains(&struct_name) { - let has_signer = - self.check_accounts_struct(&struct_name, item_struct); - debug!( - "Found external context definition: {} in {:?} with direct signer: {}", - struct_name, file_path, has_signer - ); - } - } - } - } - } - } - } - } - } - - /// Walk directory recursively to find files with given extensions - fn walk_directory( - &self, - dir: &Path, - extensions: &[&str], - ) -> Result, std::io::Error> { - let mut files = Vec::new(); - self.walk_directory_recursive(dir, extensions, &mut files)?; - Ok(files) - } - - /// Recursive helper for walking directories - fn walk_directory_recursive( - &self, - dir: &Path, - extensions: &[&str], - 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 that shouldn't be scanned - if let Some(dir_name) = path.file_name().and_then(|n| n.to_str()) { - if matches!( - dir_name, - "target" | "node_modules" | ".git" | ".vscode" | "out" - ) { - continue; - } - } - self.walk_directory_recursive(&path, extensions, files)?; - } else if let Some(ext) = path.extension().and_then(|e| e.to_str()) { - if extensions.contains(&ext) { - files.push(path); - } - } - } - - Ok(()) - } -} - -impl Detector for MissingSignerDetector { - fn id(&self) -> &'static str { - "MISSING_SIGNER" - } - - fn name(&self) -> &'static str { - "Missing Signer Check" - } - - fn description(&self) -> &'static str { - "Detects Anchor programs that have no signer accounts, which could allow unauthorized access" - } - - fn message(&self) -> &'static str { - "Program instruction has no signer. Consider adding a Signer<'info> field to ensure proper authorization." - } - - fn default_severity(&self) -> DiagnosticSeverity { - DiagnosticSeverity::WARNING - } - - fn analyze(&mut self, content: &str, file_path: Option<&PathBuf>) -> Vec { - self.diagnostics.clear(); - self.contexts.clear(); - self.context_has_signer.clear(); - self.composite_contexts.clear(); - self.processed_contexts.clear(); - self.file_path = file_path.cloned(); - - // If we don't have a workspace root yet, try to infer it from the file path - if self.workspace_root.is_none() && file_path.is_some() { - if let Some(parent) = file_path.and_then(|p| p.parent()) { - // Try to find a reasonable workspace root (going up to 5 levels) - let mut current = parent; - for _ in 0..5 { - let cargo_toml = current.join("Cargo.toml"); - if cargo_toml.exists() { - self.workspace_root = Some(current.to_path_buf()); - break; - } - if let Some(next) = current.parent() { - current = next; - } else { - break; - } - } - } - } - - info!("Starting Missing Signer analysis"); - - // Basic parsing of the file - if let Ok(syntax_tree) = parse_str::(content) { - // First pass: find all account structs and check for signer fields - self.visit_file_for_accounts(&syntax_tree); - - // Second pass: find all program functions and check their contexts - self.visit_file(&syntax_tree); - - // Search for context definitions in other files - self.search_for_context_definitions(); - - // Generate diagnostics for contexts without signers - self.generate_diagnostics(); - } - - self.diagnostics.clone() - } -} - -impl MissingSignerDetector { - fn visit_file_for_accounts(&mut self, file: &syn::File) { - for item in &file.items { - if let syn::Item::Struct(item_struct) = item { - if AnchorPatterns::is_accounts_struct(item_struct) { - let struct_name = item_struct.ident.to_string(); - let has_signer = self.check_accounts_struct(&struct_name, item_struct); - debug!( - "Found accounts struct: {} with direct signer: {}", - struct_name, has_signer - ); - } - } - } - } - - fn generate_diagnostics(&mut self) { - for (context, range) in &self.contexts { - // Check if we know about this context - if let Some(has_signer) = self.context_has_signer.get(context) { - if !*has_signer { - // This context has no signer field - let severity = self - .config - .severity_override - .unwrap_or(self.default_severity()); - - // Check if it's a composite context - if let Some(components) = self.composite_contexts.get(context) { - let components_str = components.join(", "); - self.diagnostics.push(DiagnosticBuilder::create( - *range, - format!( - "Context '{}' and its components ({}) have no signer field. Consider adding a Signer<'info> field to ensure proper authorization.", - context, components_str - ), - severity, - self.id().to_string(), - None, - )); - } else { - self.diagnostics.push(DiagnosticBuilder::create( - *range, - format!( - "Context '{}' has no signer field. Consider adding a Signer<'info> field to ensure proper authorization.", - context - ), - severity, - self.id().to_string(), - None, - )); - } - } - } else { - debug!("Context not found: {}", context); - // Context not found in any file - report it as a potential issue - let severity = self - .config - .severity_override - .unwrap_or(self.default_severity()); - - self.diagnostics.push(DiagnosticBuilder::create( - *range, - format!( - "Context '{}' definition not found. If it exists, ensure it has a Signer<'info> field for proper authorization.", - context - ), - severity, - self.id().to_string(), - None, - )); - } - } - } -} - -impl<'ast> Visit<'ast> for MissingSignerDetector { - fn visit_item_mod(&mut self, node: &'ast syn::ItemMod) { - if AnchorPatterns::is_program_module(node) { - for item in node.content.as_ref().unwrap().1.clone() { - match item { - syn::Item::Fn(item_fn) => { - self.visit_item_fn(&item_fn); - } - _ => continue, - } - } - } - } - - fn visit_item_fn(&mut self, node: &'ast syn::ItemFn) { - if let syn::Visibility::Public(_) = node.vis { - for param in &node.sig.inputs { - if let Some(inner_segment) = AnchorPatterns::extract_context_type(param) { - let context_name = inner_segment.ident.to_string(); - debug!("Found inner segment Ident: {}", context_name); - - // Store the context name and its location - let range = DiagnosticBuilder::create_range_from_span(inner_segment.span()); - self.contexts.insert(context_name, range); - } - } - } - } - - fn visit_file(&mut self, file: &'ast syn::File) { - for item in &file.items { - match item { - syn::Item::Mod(item_mod) => { - self.visit_item_mod(item_mod); - } - _ => continue, - } - } - } -} diff --git a/language-server/src/core/detectors/mod.rs b/language-server/src/core/detectors/mod.rs index 24e046f..ea38543 100644 --- a/language-server/src/core/detectors/mod.rs +++ b/language-server/src/core/detectors/mod.rs @@ -6,9 +6,7 @@ pub mod instruction_attribute_unused; pub mod manual_lamports_zeroing; pub mod missing_check_comment; pub mod missing_initspace_detector; -pub mod missing_signer; pub mod sysvar_account_detector; -pub mod unsafe_math; pub use immutable_account_mutated_detector::*; pub use instruction_attribute_invalid::*; @@ -16,6 +14,4 @@ pub use instruction_attribute_unused::*; pub use manual_lamports_zeroing::*; pub use missing_check_comment::*; pub use missing_initspace_detector::*; -pub use missing_signer::*; pub use sysvar_account_detector::*; -pub use unsafe_math::*;