diff --git a/libs/ast-extractor/.idea/.gitignore b/libs/ast-extractor/.idea/.gitignore new file mode 100644 index 00000000..13566b81 --- /dev/null +++ b/libs/ast-extractor/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/libs/ast-extractor/.idea/vcs.xml b/libs/ast-extractor/.idea/vcs.xml new file mode 100644 index 00000000..b2bdec2d --- /dev/null +++ b/libs/ast-extractor/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/toolchains/solidity/linter/core/solidhunter-lib/src/linter.rs b/toolchains/solidity/linter/core/solidhunter-lib/src/linter.rs index ec431199..cf831efc 100644 --- a/toolchains/solidity/linter/core/solidhunter-lib/src/linter.rs +++ b/toolchains/solidity/linter/core/solidhunter-lib/src/linter.rs @@ -8,7 +8,7 @@ use std::fs; use glob::glob; -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct SolidFile { pub data: ast_extractor::File, pub path: String, diff --git a/toolchains/solidity/linter/core/solidhunter-lib/src/rules/order/mod.rs b/toolchains/solidity/linter/core/solidhunter-lib/src/rules/order/mod.rs index a22ad358..cc0c65c1 100644 --- a/toolchains/solidity/linter/core/solidhunter-lib/src/rules/order/mod.rs +++ b/toolchains/solidity/linter/core/solidhunter-lib/src/rules/order/mod.rs @@ -3,19 +3,22 @@ use std::collections::HashMap; #[macro_use] pub(crate) mod import_on_top; +pub(crate) mod ordering; // List all rules use crate::rules::order::import_on_top::ImportOnTop; +use crate::rules::order::ordering::Ordering; use crate::rules::RuleBuilder; pub fn create_default_rules() -> Vec { - vec![ImportOnTop::create_default()] + vec![ImportOnTop::create_default(), Ordering::create_default()] } pub fn create_rules() -> RulesMap { let mut rules: HashMap = HashMap::new(); rules.insert(import_on_top::RULE_ID.to_string(), ImportOnTop::create); + rules.insert(ordering::RULE_ID.to_string(), Ordering::create); rules } diff --git a/toolchains/solidity/linter/core/solidhunter-lib/src/rules/order/ordering.rs b/toolchains/solidity/linter/core/solidhunter-lib/src/rules/order/ordering.rs new file mode 100644 index 00000000..c4a67c95 --- /dev/null +++ b/toolchains/solidity/linter/core/solidhunter-lib/src/rules/order/ordering.rs @@ -0,0 +1,504 @@ +use std::collections::HashMap; + +use ast_extractor::{Spanned, Visit, visit, FunctionKind, Visibility}; + +use crate::linter::SolidFile; +use crate::rules::types::*; +use crate::types::*; + +pub const RULE_ID: &str = "ordering"; +const MESSAGE: &str = "Import must be on top in the file"; + +struct OrderingVisitor { + file: SolidFile, + data: RuleEntry, + authorized_file_items: HashMap>>, + authorized_contract_items: HashMap>>, + file_current_item: Option, + contract_current_item: Option, + inside_contract: bool, + reports: Vec, +} + +impl OrderingVisitor { + fn new(file: SolidFile, data: RuleEntry) -> OrderingVisitor { + let authorized_file_items: HashMap>> = [ + (FileItemType::Pragma, vec![None]), + (FileItemType::Import, vec![None, Some(FileItemType::Pragma)]), + ( + FileItemType::Enum, + vec![ + None, + Some(FileItemType::Pragma), + Some(FileItemType::Import), + ], + ), + ( + FileItemType::Struct, + vec![ + None, + Some(FileItemType::Pragma), + Some(FileItemType::Import), + Some(FileItemType::Enum), + ], + ), + ( + FileItemType::ContractInterface, + vec![ + None, + Some(FileItemType::Pragma), + Some(FileItemType::Import), + Some(FileItemType::Enum), + Some(FileItemType::Struct), + ], + ), + ( + FileItemType::ContractLibrary, + vec![ + None, + Some(FileItemType::Pragma), + Some(FileItemType::Import), + Some(FileItemType::Enum), + Some(FileItemType::Struct), + Some(FileItemType::ContractInterface), + ], + ), + ( + FileItemType::Contract, + vec![ + None, + Some(FileItemType::Pragma), + Some(FileItemType::Import), + Some(FileItemType::Enum), + Some(FileItemType::Struct), + Some(FileItemType::ContractInterface), + Some(FileItemType::ContractLibrary), + ], + ), + ].iter() + .cloned() + .collect(); + + let authorized_contract_items: HashMap>> = [ + (ContractItemType::Udt, vec![None]), + ( + ContractItemType::Struct, + vec![None, Some(ContractItemType::Udt)], + ), + ( + ContractItemType::Enum, + vec![None, Some(ContractItemType::Udt), Some(ContractItemType::Struct)], + ), + ( + ContractItemType::Property, + vec![ + None, + Some(ContractItemType::Udt), + Some(ContractItemType::Struct), + Some(ContractItemType::Enum), + ], + ), + ( + ContractItemType::Event, + vec![ + None, + Some(ContractItemType::Udt), + Some(ContractItemType::Struct), + Some(ContractItemType::Enum), + Some(ContractItemType::Property), + ], + ), + ( + ContractItemType::Modifier, + vec![ + None, + Some(ContractItemType::Udt), + Some(ContractItemType::Struct), + Some(ContractItemType::Enum), + Some(ContractItemType::Property), + Some(ContractItemType::Event), + ], + ), + ( + ContractItemType::Constructor, + vec![ + None, + Some(ContractItemType::Udt), + Some(ContractItemType::Struct), + Some(ContractItemType::Enum), + Some(ContractItemType::Property), + Some(ContractItemType::Event), + Some(ContractItemType::Modifier), + ], + ), + ( + ContractItemType::Receive, + vec![ + None, + Some(ContractItemType::Udt), + Some(ContractItemType::Struct), + Some(ContractItemType::Enum), + Some(ContractItemType::Property), + Some(ContractItemType::Event), + Some(ContractItemType::Modifier), + Some(ContractItemType::Constructor), + ], + ), + ( + ContractItemType::FallBack, + vec![ + None, + Some(ContractItemType::Udt), + Some(ContractItemType::Struct), + Some(ContractItemType::Enum), + Some(ContractItemType::Property), + Some(ContractItemType::Event), + Some(ContractItemType::Modifier), + Some(ContractItemType::Constructor), + Some(ContractItemType::Receive), + ], + ), + ( + ContractItemType::ExternalFunction, + vec![ + None, + Some(ContractItemType::Udt), + Some(ContractItemType::Struct), + Some(ContractItemType::Enum), + Some(ContractItemType::Property), + Some(ContractItemType::Event), + Some(ContractItemType::Modifier), + Some(ContractItemType::Constructor), + Some(ContractItemType::Receive), + Some(ContractItemType::FallBack), + ], + ), + ( + ContractItemType::PublicFunction, + vec![ + None, + Some(ContractItemType::Udt), + Some(ContractItemType::Struct), + Some(ContractItemType::Enum), + Some(ContractItemType::Property), + Some(ContractItemType::Event), + Some(ContractItemType::Modifier), + Some(ContractItemType::Constructor), + Some(ContractItemType::Receive), + Some(ContractItemType::FallBack), + Some(ContractItemType::ExternalFunction), + ], + ), + ( + ContractItemType::InternalFunction, + vec![ + None, + Some(ContractItemType::Udt), + Some(ContractItemType::Struct), + Some(ContractItemType::Enum), + Some(ContractItemType::Property), + Some(ContractItemType::Event), + Some(ContractItemType::Modifier), + Some(ContractItemType::Constructor), + Some(ContractItemType::Receive), + Some(ContractItemType::FallBack), + Some(ContractItemType::ExternalFunction), + Some(ContractItemType::PublicFunction), + ], + ), + ( + ContractItemType::PrivateFunction, + vec![ + None, + Some(ContractItemType::Udt), + Some(ContractItemType::Struct), + Some(ContractItemType::Enum), + Some(ContractItemType::Property), + Some(ContractItemType::Event), + Some(ContractItemType::Modifier), + Some(ContractItemType::Constructor), + Some(ContractItemType::Receive), + Some(ContractItemType::FallBack), + Some(ContractItemType::ExternalFunction), + Some(ContractItemType::PublicFunction), + Some(ContractItemType::InternalFunction), + ], + ), + ].iter() + .cloned() + .collect(); + OrderingVisitor { + file, + data, + authorized_file_items, + authorized_contract_items, + file_current_item: None, + contract_current_item: None, + inside_contract: false, + reports: Vec::new(), + } + } + + fn create_diag( + &self, + file: &SolidFile, + location: (ast_extractor::LineColumn, ast_extractor::LineColumn), + ) -> LintDiag { + let range = Range { + start: Position { + line: location.0.line, + character: location.0.column, + }, + end: Position { + line: location.1.line, + character: location.1.column, + }, + }; + LintDiag { + id: RULE_ID.to_string(), + range, + message: MESSAGE.to_string(), + severity: Some(self.data.severity), + code: None, + source: None, + uri: file.path.clone(), + source_file_content: file.content.clone(), + } + } + + fn is_authorized_file_item(&self, item: FileItemType) -> bool { + if let Some(authorized_items) = self.authorized_file_items.get(&item) { + let res = authorized_items.contains(&self.file_current_item); + return res; + } + true + } + + fn is_authorized_contract_item(&self, item: ContractItemType) -> bool { + if let Some(authorized_items) = self.authorized_contract_items.get(&item) { + return authorized_items.contains(&self.contract_current_item); + } + true + } +} + +impl<'ast> Visit<'ast> for OrderingVisitor { + fn visit_pragma_directive(&mut self, pragma: &'ast ast_extractor::PragmaDirective) { + if !self.is_authorized_file_item(FileItemType::Pragma) { + let location = (pragma.span().start(), pragma.span().end()); + self.reports.push(self.create_diag(&self.file, location)); + } else { + self.file_current_item = Some(FileItemType::Pragma); + } + } + + fn visit_import_directive(&mut self, import: &'ast ast_extractor::ImportDirective) { + if !self.is_authorized_file_item(FileItemType::Import) { + let location = (import.span().start(), import.span().end()); + self.reports.push(self.create_diag(&self.file, location)); + } else { + self.file_current_item = Some(FileItemType::Import); + } + } + + fn visit_item_enum(&mut self, enum_def: &'ast ast_extractor::ItemEnum) { + if !self.is_authorized_file_item(FileItemType::Enum) { + let location = (enum_def.span().start(), enum_def.span().end()); + self.reports.push(self.create_diag(&self.file, location)); + } else { + self.file_current_item = Some(FileItemType::Enum); + } + } + + fn visit_item_struct(&mut self, struct_def: &'ast ast_extractor::ItemStruct) { + if !self.is_authorized_file_item(FileItemType::Struct) { + let location = (struct_def.span().start(), struct_def.span().end()); + self.reports.push(self.create_diag(&self.file, location)); + } else { + self.file_current_item = Some(FileItemType::Struct); + } + } + + fn visit_item_contract(&mut self, contract_def: &'ast ast_extractor::ItemContract) { + if contract_def.is_interface() && !self.is_authorized_file_item(FileItemType::ContractInterface) || + contract_def.is_library() && !self.is_authorized_file_item(FileItemType::ContractLibrary) || + !self.is_authorized_file_item(FileItemType::Contract) { + let location = (contract_def.span().start(), contract_def.span().end()); + self.reports.push(self.create_diag(&self.file, location)); + } else { + self.file_current_item = Some(FileItemType::Contract); + } + self.contract_current_item = None; + self.inside_contract = true; + visit::visit_item_contract(self, contract_def); + self.inside_contract = false; + } + + fn visit_item_udt(&mut self,udt: &'ast ast_extractor::ItemUdt) { + if !self.is_authorized_contract_item(ContractItemType::Udt) { + let location = (udt.span().start(), udt.span().end()); + self.reports.push(self.create_diag(&self.file, location)); + } else { + self.contract_current_item = Some(ContractItemType::Udt); + } + } + + fn visit_variable_definition(&mut self,var: &'ast ast_extractor::VariableDefinition) { + if !self.is_authorized_contract_item(ContractItemType::Property) { + let location = (var.span().start(), var.span().end()); + self.reports.push(self.create_diag(&self.file, location)); + } else { + self.contract_current_item = Some(ContractItemType::Property); + } + } + + fn visit_item_event(&mut self,event: &'ast ast_extractor::ItemEvent) { + if !self.is_authorized_contract_item(ContractItemType::Event) { + let location = (event.span().start(), event.span().end()); + self.reports.push(self.create_diag(&self.file, location)); + } else { + self.contract_current_item = Some(ContractItemType::Event); + } + } + + fn visit_item_function(&mut self,function: &'ast ast_extractor::ItemFunction) { + + match function.kind { + FunctionKind::Modifier(_) => { + if !self.is_authorized_contract_item(ContractItemType::Modifier) { + let location = (function.span().start(), function.span().end()); + self.reports.push(self.create_diag(&self.file, location)); + } else { + self.contract_current_item = Some(ContractItemType::Modifier); + } + }, + FunctionKind::Constructor(_) => { + if !self.is_authorized_contract_item(ContractItemType::Constructor) { + let location = (function.span().start(), function.span().end()); + self.reports.push(self.create_diag(&self.file, location)); + } else { + self.contract_current_item = Some(ContractItemType::Constructor); + } + }, + FunctionKind::Receive(_) => { + if !self.is_authorized_contract_item(ContractItemType::Receive) { + let location = (function.span().start(), function.span().end()); + self.reports.push(self.create_diag(&self.file, location)); + } else { + self.contract_current_item = Some(ContractItemType::Receive); + } + }, + FunctionKind::Fallback(_) => { + if !self.is_authorized_contract_item(ContractItemType::FallBack) { + let location = (function.span().start(), function.span().end()); + self.reports.push(self.create_diag(&self.file, location)); + } else { + self.contract_current_item = Some(ContractItemType::FallBack); + } + }, + FunctionKind::Function(_) => { + let visibility = function.attributes.iter().find(|attr| matches!(attr, ast_extractor::FunctionAttribute::Visibility(_))); + let visibility = match visibility { + Some(ast_extractor::FunctionAttribute::Visibility(visibility)) => visibility, + _ => return, + }; + + match visibility { + Visibility::External(_) => { + if !self.is_authorized_contract_item(ContractItemType::ExternalFunction) { + let location = (function.span().start(), function.span().end()); + self.reports.push(self.create_diag(&self.file, location)); + } else { + self.contract_current_item = Some(ContractItemType::ExternalFunction); + } + }, + Visibility::Public(_) => { + if !self.is_authorized_contract_item(ContractItemType::PublicFunction) { + let location = (function.span().start(), function.span().end()); + self.reports.push(self.create_diag(&self.file, location)); + } else { + self.contract_current_item = Some(ContractItemType::PublicFunction); + } + }, + Visibility::Internal(_) => { + if !self.is_authorized_contract_item(ContractItemType::InternalFunction) { + let location = (function.span().start(), function.span().end()); + self.reports.push(self.create_diag(&self.file, location)); + } else { + self.contract_current_item = Some(ContractItemType::InternalFunction); + } + }, + Visibility::Private(_) => { + if !self.is_authorized_contract_item(ContractItemType::PrivateFunction) { + let location = (function.span().start(), function.span().end()); + self.reports.push(self.create_diag(&self.file, location)); + } else { + self.contract_current_item = Some(ContractItemType::PrivateFunction); + } + }, + } + + } + } + } + + +} + +#[derive(Debug, Clone)] +pub struct Ordering { + data: RuleEntry, +} + + +impl RuleType for Ordering { + fn diagnose(&self, file: &SolidFile, _files: &[SolidFile]) -> Vec { + let mut visitor = OrderingVisitor::new(file.clone(), self.data.clone()); + visitor.visit_file(&file.data); + visitor.reports + } +} + +impl Ordering { + pub(crate) fn create(data: RuleEntry) -> Box { + let rule = Ordering { data }; + Box::new(rule) + } + + pub(crate) fn create_default() -> RuleEntry { + RuleEntry { + id: RULE_ID.to_string(), + severity: Severity::WARNING, + data: vec![], + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +enum FileItemType { + Pragma, + Import, + Enum, + Struct, + ContractInterface, + ContractLibrary, + Contract, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +enum ContractItemType { + Udt, + Struct, + Enum, + Property, + Event, + Modifier, + Constructor, + Receive, + FallBack, + ExternalFunction, + PublicFunction, + InternalFunction, + PrivateFunction, +} + diff --git a/toolchains/solidity/linter/core/solidhunter-lib/src/rules/types.rs b/toolchains/solidity/linter/core/solidhunter-lib/src/rules/types.rs index f18dedcf..08d0c0f9 100644 --- a/toolchains/solidity/linter/core/solidhunter-lib/src/rules/types.rs +++ b/toolchains/solidity/linter/core/solidhunter-lib/src/rules/types.rs @@ -3,7 +3,7 @@ use crate::types::*; use serde::{Deserialize, Serialize}; use std::collections::HashMap; -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, Clone)] pub struct RuleEntry { pub id: String, pub severity: Severity, diff --git a/toolchains/solidity/linter/core/solidhunter-lib/testdata/Ordering/.solidhunter.json b/toolchains/solidity/linter/core/solidhunter-lib/testdata/Ordering/.solidhunter.json new file mode 100644 index 00000000..8cd1f7d2 --- /dev/null +++ b/toolchains/solidity/linter/core/solidhunter-lib/testdata/Ordering/.solidhunter.json @@ -0,0 +1,12 @@ +{ + "name": "solidhunter", + "includes": [], + "plugins": [], + "rules": [ + { + "id": "ordering", + "severity": "WARNING", + "data": [] + } + ] +} \ No newline at end of file diff --git a/toolchains/solidity/linter/core/solidhunter-lib/testdata/Ordering/file.sol b/toolchains/solidity/linter/core/solidhunter-lib/testdata/Ordering/file.sol new file mode 100644 index 00000000..f3584746 --- /dev/null +++ b/toolchains/solidity/linter/core/solidhunter-lib/testdata/Ordering/file.sol @@ -0,0 +1,12 @@ +pragma solidity 0.8.19; + + +contract MyContract { + function foo() public {} + + using MyMathLib for uint; // NOK + + uint a; // NOK +} + +library MyLibrary {} // NOK \ No newline at end of file diff --git a/toolchains/solidity/linter/core/solidhunter-lib/testdata/Ordering/findings.csv b/toolchains/solidity/linter/core/solidhunter-lib/testdata/Ordering/findings.csv new file mode 100644 index 00000000..fb25cf75 --- /dev/null +++ b/toolchains/solidity/linter/core/solidhunter-lib/testdata/Ordering/findings.csv @@ -0,0 +1,2 @@ +ordering:9:4:9:11 +ordering:12:8:12:17 \ No newline at end of file diff --git a/toolchains/solidity/linter/core/solidhunter-lib/tests/linter.rs b/toolchains/solidity/linter/core/solidhunter-lib/tests/linter.rs index a123add4..1f129cd1 100644 --- a/toolchains/solidity/linter/core/solidhunter-lib/tests/linter.rs +++ b/toolchains/solidity/linter/core/solidhunter-lib/tests/linter.rs @@ -167,4 +167,5 @@ test_directories! { GlobalImport, NotRelyOnTime, NamedParametersMapping, + Ordering }