From bd8df90fdb8410d2cd3b84875cbaa66ccb082685 Mon Sep 17 00:00:00 2001 From: Thomas Johannesson Date: Fri, 12 Sep 2025 17:28:56 +0200 Subject: [PATCH 01/13] Get argument types from source to handle unknown types Tests updated since reference and pointer types are formatted as the source now. --- Cargo.toml | 4 ++-- src/model.rs | 37 ++++++++++++++++++++++++++++++---- tests/lib_integration_tests.rs | 4 ++-- 3 files changed, 37 insertions(+), 8 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 2ad1562..51a4a7c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,12 +26,12 @@ thiserror = "2.0" # instructing clang-sys where libclang is located. For some reason, clang-sys does a # better job at finding libclang this way. [target.'cfg(not(target_os = "windows"))'.dependencies] -clang = { version="2.0", features=["runtime", "clang_5_0"] } +clang = { version="2.0", features=["runtime", "clang_6_0"] } # There is an issue with clang 19 and 20 on Windows when using the runtime feature. # Unloading of DLL causes segfault when the clang::Clang object is droppped. [target.'cfg(target_os = "windows")'.dependencies] -clang = { version="2.0", features=["clang_5_0"] } +clang = { version="2.0", features=["clang_6_0"] } [dev-dependencies] diff --git a/src/model.rs b/src/model.rs index 342c8ef..04bd2d2 100644 --- a/src/model.rs +++ b/src/model.rs @@ -67,10 +67,7 @@ impl MethodToMock { .expect("Method should have arguments") .iter() .map(|arg| Argument { - type_name: arg - .get_type() - .expect("Argument should have a type") - .get_display_name(), + type_name: Self::get_type(arg), name: arg.get_name(), }) .collect(), @@ -86,6 +83,38 @@ impl MethodToMock { ), } } + + fn get_type(entity: &clang::Entity) -> String { + Self::extract_type_from_source(entity).unwrap_or_else(|| { + entity + .get_type() + .expect("Entity should have a type") + .get_display_name() + }) + } + + fn extract_type_from_source(entity: &clang::Entity) -> Option { + if let Some(range) = entity.get_range() { + // TODO: Don't get source code every time! + if let Some(loc) = entity.get_location() + && let Some(file) = loc.get_file_location().file + && let Some(contents) = file.get_contents() + { + let start = range.get_start().get_file_location().offset as usize; + let mut end = range.get_end().get_file_location().offset as usize; + if let Some(name) = entity.get_name() { + end -= name.len(); + } + + if start >= end || end > contents.len() { + // TODO: warn, sanity check + return None; + } + return Some(contents[start..end].trim().to_string()); + } + } + None + } } struct AstTraverser<'a> { diff --git a/tests/lib_integration_tests.rs b/tests/lib_integration_tests.rs index c7b84ac..a8cb16e 100644 --- a/tests/lib_integration_tests.rs +++ b/tests/lib_integration_tests.rs @@ -55,7 +55,7 @@ fn various_return_types_and_argument_types_can_be_mocked() { "class MockFoo : public Foo", "{", "public:", - " MOCK_METHOD(std::string, bar, (const std::string & arg1, const char * arg2), (override));", + " MOCK_METHOD(std::string, bar, (const std::string& arg1, const char* arg2), (override));", " MOCK_METHOD(uint32_t, fizz, (uint32_t arg1, uint64_t arg2, int32_t arg3, int64_t arg4), (override));", "};" ) @@ -128,7 +128,7 @@ fn types_with_commas_are_wrapped_with_parenthesis() { "class MockFoo : public Foo", "{", "public:", - " MOCK_METHOD((std::map), bar, ((const std::map & arg)), (override));", + " MOCK_METHOD((std::map), bar, ((const std::map& arg)), (override));", "};" ) ); From fbc78afb32f5dcc001ae6430c911c4e67b4d970c Mon Sep 17 00:00:00 2001 From: Thomas Johannesson Date: Wed, 31 Dec 2025 12:45:56 +0100 Subject: [PATCH 02/13] Put logger in Rc to be able to use it in several places --- src/clangwrap.rs | 13 +++++++------ src/lib.rs | 15 ++++++++++----- src/log.rs | 9 +++++---- 3 files changed, 22 insertions(+), 15 deletions(-) diff --git a/src/clangwrap.rs b/src/clangwrap.rs index 9112aa5..f38a099 100644 --- a/src/clangwrap.rs +++ b/src/clangwrap.rs @@ -3,6 +3,7 @@ use crate::{log, verbose}; use capitalize::Capitalize; use std::{ path::{Path, PathBuf}, + rc::Rc, sync::{Mutex, MutexGuard, TryLockError}, }; @@ -16,7 +17,7 @@ static DUMMY_FILE: &str = "mocksmith_dummy_input_file.h"; // Struct to wrap the Clang library and a mutex guard to ensure only one thread can use it // at a time, at least via this library. pub(crate) struct ClangWrap { - log: Option, + log: Rc>, clang: clang::Clang, // After clang::Clang to ensure releasing lock after Clang is dropped _clang_lock: MutexGuard<'static, ()>, @@ -31,7 +32,7 @@ impl ClangWrap { CLANG_MUTEX.clear_poison(); } - pub(crate) fn new(log: Option) -> crate::Result { + pub(crate) fn new(log: Rc>) -> crate::Result { let clang_lock = CLANG_MUTEX.try_lock().map_err(|error| match error { TryLockError::WouldBlock => crate::MocksmithError::Busy, TryLockError::Poisoned(_) => MocksmithError::Poisoned, @@ -41,12 +42,12 @@ impl ClangWrap { pub(crate) fn blocking_new() -> crate::Result { let clang_lock = CLANG_MUTEX.lock().map_err(|_| MocksmithError::Poisoned)?; - Self::create(clang_lock, None) + Self::create(clang_lock, Rc::new(None)) } fn create( clang_lock: MutexGuard<'static, ()>, - log: Option, + log: Rc>, ) -> crate::Result { let clang = clang::Clang::new().map_err(MocksmithError::ClangError)?; // Create clang object before getting version to ensure libclang is loaded @@ -133,11 +134,11 @@ impl ClangWrap { .filter(|diagnostic| { diagnostic.get_severity() >= clang::diagnostic::Severity::Error }) - .for_each(|diagnostic| log!(&self.log, "{}", diagnostic)); + .for_each(|diagnostic| log!(self.log, "{}", diagnostic)); } else { diagnostics .iter() - .for_each(|diagnostic| verbose!(&self.log, "{}", diagnostic)); + .for_each(|diagnostic| verbose!(self.log, "{}", diagnostic)); } if !self.ignore_errors { diff --git a/src/lib.rs b/src/lib.rs index f85c156..7cf9ecd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,7 +7,10 @@ pub mod naming; use clangwrap::ClangWrap; use headerpath::header_include_path; -use std::path::{Path, PathBuf}; +use std::{ + path::{Path, PathBuf}, + rc::Rc, +}; #[derive(thiserror::Error, Debug, PartialEq)] pub enum MocksmithError { @@ -87,6 +90,7 @@ impl crate::MockHeader { /// Mocksmith is a struct for generating Google Mock mocks for C++ classes. pub struct Mocksmith { + log: Rc>, clangwrap: ClangWrap, generator: generate::Generator, @@ -102,8 +106,8 @@ impl Mocksmith { /// The function fails if another thread already holds an instance, since Clang can /// only be used from one thread. pub fn new(log_write: Option>, verbose: bool) -> Result { - let log = log_write.map(|write| log::Logger::new(write, verbose)); - Self::create(ClangWrap::new(log)?) + let log = Rc::new(log_write.map(|write| log::Logger::new(write, verbose))); + Self::create(Rc::clone(&log), ClangWrap::new(log)?) } /// Creates a new Mocksmith instance. @@ -117,12 +121,13 @@ impl Mocksmith { ClangWrap::clear_poison(); clangwrap = ClangWrap::blocking_new(); } - Self::create(clangwrap?) + Self::create(Rc::new(None), clangwrap?) } - fn create(clangwrap: clangwrap::ClangWrap) -> Result { + fn create(log: Rc>, clangwrap: clangwrap::ClangWrap) -> Result { let methods_to_mock = MethodsToMockStrategy::AllVirtual; let mocksmith = Self { + log, clangwrap, generator: generate::Generator::new(methods_to_mock), include_paths: Vec::new(), diff --git a/src/log.rs b/src/log.rs index ea462f1..349c725 100644 --- a/src/log.rs +++ b/src/log.rs @@ -3,7 +3,7 @@ use std::{cell::RefCell, io::Write}; #[macro_export] macro_rules! log { ($logger:expr, $($arg:tt)*) => { - if let Some(logger) = &$logger { + if let Some(logger) = &*$logger { logger.log(&format!($($arg)*)); } }; @@ -12,7 +12,7 @@ macro_rules! log { #[macro_export] macro_rules! verbose { ($logger:expr, $($arg:tt)*) => { - if let Some(logger) = &$logger { + if let Some(logger) = &*$logger { if logger.verbose { logger.log(&format!($($arg)*)); } @@ -42,6 +42,7 @@ impl Logger { #[cfg(test)] mod tests { use super::*; + use std::rc::Rc; #[test] fn macro_doesnt_evaluate_args_if_verbose_disabled() { @@ -52,7 +53,7 @@ mod tests { }; let write = Box::new(Vec::::new()); - let log = Some(Logger::new(write, false)); + let log = Rc::new(Some(Logger::new(write, false))); verbose!(log, "{}", fun()); assert_eq!(calls, 0); } @@ -66,7 +67,7 @@ mod tests { }; let write = Box::new(Vec::::new()); - let log = Some(Logger::new(write, true)); + let log = Rc::new(Some(Logger::new(write, true))); verbose!(log, "{}", fun()); assert_eq!(calls, 1); } From c270285a0bd958dbf0f52ae7450bd57b6394e429 Mon Sep 17 00:00:00 2001 From: Thomas Johannesson Date: Fri, 12 Sep 2025 17:29:31 +0200 Subject: [PATCH 03/13] Introduce ModelFactory to be able to cache file contents of translation unit It seems the file contents is not available in libclang entites until we have actually traversed the AST a bit. Thus we extract the source file contents for the translation unit when accessing the first entity. The file contents is cached in the new class `ModelFactory` used instead of having methods directly in `ClassToMock`. --- src/lib.rs | 3 +- src/model.rs | 87 ++++++++++++++++++++++++++++++++++++++-------------- 2 files changed, 66 insertions(+), 24 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 7cf9ecd..1c50d3d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -285,7 +285,8 @@ impl Mocksmith { } fn create_mocks(&self, tu: &clang::TranslationUnit) -> Result> { - let classes = model::classes_in_translation_unit(tu, self.methods_to_mock); + let classes = + model::classes_in_translation_unit(Rc::clone(&self.log), tu, self.methods_to_mock); Ok(classes .iter() .filter(|class| (self.filter_class)(class.name.as_str())) diff --git a/src/model.rs b/src/model.rs index 04bd2d2..2f10ad4 100644 --- a/src/model.rs +++ b/src/model.rs @@ -1,3 +1,6 @@ +use crate::log; +use std::rc::Rc; + // Represents a class that shall be mocked #[derive(Debug)] pub(crate) struct ClassToMock { @@ -6,6 +9,7 @@ pub(crate) struct ClassToMock { pub(crate) methods: Vec, } +// Represents a class method that shall be mocked #[derive(Debug)] pub(crate) struct MethodToMock { pub(crate) name: String, @@ -17,6 +21,7 @@ pub(crate) struct MethodToMock { pub(crate) ref_qualifier: Option, } +// Represents a method argument #[derive(Debug)] pub(crate) struct Argument { pub(crate) type_name: String, @@ -25,19 +30,34 @@ pub(crate) struct Argument { // Finds classes to mock in the main file of a translation unit pub(crate) fn classes_in_translation_unit( + log: Rc>, root: &clang::TranslationUnit, methods_to_mock: crate::MethodsToMockStrategy, ) -> Vec { - AstTraverser::new(root, methods_to_mock).traverse() + AstTraverser::new(log, root, methods_to_mock).traverse() +} + +// Factory for creating model objects from clang entities +struct ModelFactory { + log: Rc>, + file_contents: Option, } -impl ClassToMock { - fn from_entity( +impl ModelFactory { + fn new(log: Rc>) -> Self { + Self { + log, + file_contents: None, + } + } + + fn class_from_entity( + &mut self, class: &clang::Entity, namespaces: &Vec, methods_to_mock: crate::MethodsToMockStrategy, - ) -> Self { - Self { + ) -> ClassToMock { + ClassToMock { name: class.get_name().expect("Class should have a name"), namespaces: namespaces .iter() @@ -48,15 +68,13 @@ impl ClassToMock { .iter() .filter(|child| child.get_kind() == clang::EntityKind::Method) .filter(|method| methods_to_mock.should_mock(method)) - .map(|method| MethodToMock::from_entity(method)) + .map(|method| self.method_from_entity(method)) .collect(), } } -} -impl MethodToMock { - fn from_entity(method: &clang::Entity) -> Self { - Self { + fn method_from_entity(&mut self, method: &clang::Entity) -> MethodToMock { + MethodToMock { name: method.get_name().expect("Method should have a name"), result_type: method .get_result_type() @@ -67,7 +85,7 @@ impl MethodToMock { .expect("Method should have arguments") .iter() .map(|arg| Argument { - type_name: Self::get_type(arg), + type_name: self.get_type(arg), name: arg.get_name(), }) .collect(), @@ -84,8 +102,8 @@ impl MethodToMock { } } - fn get_type(entity: &clang::Entity) -> String { - Self::extract_type_from_source(entity).unwrap_or_else(|| { + fn get_type(&mut self, entity: &clang::Entity) -> String { + self.extract_type_from_source(entity).unwrap_or_else(|| { entity .get_type() .expect("Entity should have a type") @@ -93,32 +111,53 @@ impl MethodToMock { }) } - fn extract_type_from_source(entity: &clang::Entity) -> Option { + fn extract_type_from_source(&mut self, entity: &clang::Entity) -> Option { if let Some(range) = entity.get_range() { - // TODO: Don't get source code every time! - if let Some(loc) = entity.get_location() - && let Some(file) = loc.get_file_location().file - && let Some(contents) = file.get_contents() - { + self.cache_file_contents(entity); + + if let Some(file_contents) = &self.file_contents { let start = range.get_start().get_file_location().offset as usize; let mut end = range.get_end().get_file_location().offset as usize; if let Some(name) = entity.get_name() { end -= name.len(); } - if start >= end || end > contents.len() { - // TODO: warn, sanity check + if start >= end || end > file_contents.len() { + log!( + self.log, + "Falling back to clang type extraction for entity {:?} \ + due to illegal file position", + entity + ); return None; } - return Some(contents[start..end].trim().to_string()); + return Some(file_contents[start..end].trim().to_string()); } } + log!( + self.log, + "Falling back to clang type extraction for entity {:?} \ + due to missing range or file contents", + entity + ); None } + + fn cache_file_contents(&mut self, entity: &clang::Entity) { + if self.file_contents.is_none() { + if let Some(location) = entity.get_location() + && let Some(file) = location.get_file_location().file + { + self.file_contents = file.get_contents(); + } + } + } } +// Traverses the AST to find classes to mock struct AstTraverser<'a> { root: clang::Entity<'a>, + factory: ModelFactory, methods_to_mock: crate::MethodsToMockStrategy, classes: Vec, @@ -127,11 +166,13 @@ struct AstTraverser<'a> { impl<'a> AstTraverser<'a> { pub fn new( + log: Rc>, root: &'a clang::TranslationUnit<'a>, methods_to_mock: crate::MethodsToMockStrategy, ) -> Self { Self { root: root.get_entity(), + factory: ModelFactory::new(log), methods_to_mock, classes: Vec::new(), namespace_stack: Vec::new(), @@ -147,7 +188,7 @@ impl<'a> AstTraverser<'a> { match entity.get_kind() { clang::EntityKind::ClassDecl => { if entity.is_definition() && self.should_mock_class(&entity) { - self.classes.push(ClassToMock::from_entity( + self.classes.push(self.factory.class_from_entity( &entity, &self.namespace_stack, self.methods_to_mock, From dbbe32e016e0dbd1c2ef65a4ece5d41dcddde900 Mon Sep 17 00:00:00 2001 From: Thomas Johannesson Date: Wed, 31 Dec 2025 15:11:56 +0100 Subject: [PATCH 04/13] Fix bug in assert_mocks macro not checking lengths --- tests/assertions/mod.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/assertions/mod.rs b/tests/assertions/mod.rs index 8ae27f3..fe36098 100644 --- a/tests/assertions/mod.rs +++ b/tests/assertions/mod.rs @@ -45,6 +45,14 @@ macro_rules! assert_mocks { err ) }); + + assert_eq!(actual_mocks.len(), + expected_mocks.len(), + "Number of generated mocks ({}) does not match expected ({})", + actual_mocks.len(), + expected_mocks.len() + ); + actual_mocks.iter() .zip(expected_mocks.iter()) .for_each(|(actual, expected)| { From ba6e6fc20b74d6aa72231d1282c2ec3f11a3de4d Mon Sep 17 00:00:00 2001 From: Thomas Johannesson Date: Wed, 31 Dec 2025 15:19:10 +0100 Subject: [PATCH 05/13] Add tests of handling unknown types --- tests/lib_integration_tests.rs | 83 ++++++++++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) diff --git a/tests/lib_integration_tests.rs b/tests/lib_integration_tests.rs index a8cb16e..312f99b 100644 --- a/tests/lib_integration_tests.rs +++ b/tests/lib_integration_tests.rs @@ -199,6 +199,45 @@ fn unknown_argument_type_is_treated_as_error() { ); } +#[test] +fn unknown_argument_can_be_handled_if_ignoring_errors() { + let mocksmith = Mocksmith::new_when_available().unwrap().ignore_errors(true); + let cpp_class = " + class Foo { + public: + virtual ~Foo() = default; + virtual void bar(const Unknown& arg) = 0; + };"; + assert_mocks!( + mocksmith.create_mocks_from_string(cpp_class), + lines!( + "class MockFoo : public Foo", + "{", + "public:", + " MOCK_METHOD(void, bar, (const Unknown& arg), (override));", + "};" + ) + ); + + let cpp_class = " + class Foo { + public: + virtual ~Foo() = default; + // Include of is missing + virtual void fizz(const std::string& arg) = 0; + };"; + assert_mocks!( + mocksmith.create_mocks_from_string(cpp_class), + lines!( + "class MockFoo : public Foo", + "{", + "public:", + " MOCK_METHOD(void, fizz, (const std::string& arg), (override));", + "};" + ) + ); +} + #[test] fn unknown_return_type_is_treated_as_error() { let mocksmith = Mocksmith::new_when_available().unwrap(); @@ -221,6 +260,50 @@ fn unknown_return_type_is_treated_as_error() { ); } +#[test] +fn unknown_return_type_can_be_handled_if_ignoring_errors() { + let mocksmith = Mocksmith::new_when_available().unwrap().ignore_errors(true); + let cpp_class = " + class Foo { + public: + virtual ~Foo() = default; + virtual SomeReturnType bar() = 0; + };"; + assert_mocks!( + mocksmith.create_mocks_from_string(cpp_class), + lines!( + "class MockFoo : public Foo", + "{", + "public:", + " MOCK_METHOD(Unknown, bar, (), (override));", + "};" + ) + ); +} + +#[test] +fn unknown_types_in_file_can_be_handled_if_ignoring_errors() { + let file = temp_file_from( + " + class Foo { + public: + virtual ~Foo() = default; + virtual SomeReturnType bar(const SomeArgType& arg) = 0; + };", + ); + let mocksmith = Mocksmith::new_when_available().unwrap().ignore_errors(true); + assert_mocks!( + mocksmith.create_mocks_for_file(file.path()), + lines!( + "class MockFoo : public Foo", + "{", + "public:", + " MOCK_METHOD(SomeReturnType, bar, (const SomeArgType& arg), (override));", + "};" + ) + ); +} + #[test] fn error_in_included_file_is_reported_in_correct_file() { let dir = temp_dir(); From b97426aed76cb515472d5a5e6498de988e843358 Mon Sep 17 00:00:00 2001 From: Thomas Johannesson Date: Wed, 31 Dec 2025 15:21:32 +0100 Subject: [PATCH 06/13] SQ: clippy --- src/model.rs | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/model.rs b/src/model.rs index 2f10ad4..49098af 100644 --- a/src/model.rs +++ b/src/model.rs @@ -144,12 +144,11 @@ impl ModelFactory { } fn cache_file_contents(&mut self, entity: &clang::Entity) { - if self.file_contents.is_none() { - if let Some(location) = entity.get_location() - && let Some(file) = location.get_file_location().file - { - self.file_contents = file.get_contents(); - } + if self.file_contents.is_none() + && let Some(location) = entity.get_location() + && let Some(file) = location.get_file_location().file + { + self.file_contents = file.get_contents(); } } } From 994bb3a8d783623459b370547322f55282e6a32c Mon Sep 17 00:00:00 2001 From: Thomas Johannesson Date: Mon, 5 Jan 2026 12:14:01 +0100 Subject: [PATCH 07/13] Add some tests for creating model --- src/lib.rs | 3 +- src/model.rs | 129 ++++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 130 insertions(+), 2 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 1c50d3d..27ab4a3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -305,7 +305,8 @@ mod tests { #[test] fn test_new_with_threads() { - let mocksmith = Mocksmith::new(None, false).unwrap(); + // new_when_available to wait for other tests using ClangWrap + let mocksmith = Mocksmith::new_when_available().unwrap(); let handle = std::thread::spawn(|| { assert!(matches!( diff --git a/src/model.rs b/src/model.rs index 49098af..4ee50ad 100644 --- a/src/model.rs +++ b/src/model.rs @@ -22,7 +22,7 @@ pub(crate) struct MethodToMock { } // Represents a method argument -#[derive(Debug)] +#[derive(Debug, PartialEq)] pub(crate) struct Argument { pub(crate) type_name: String, pub(crate) name: Option, @@ -229,3 +229,130 @@ impl crate::MethodsToMockStrategy { } } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::clangwrap::ClangWrap; + + #[test] + fn class_with_methods_with_recognized_types() { + let code = r#" + class MyClass { + public: + virtual void foo() const noexcept; + int bar(int x); + virtual int baz() = 0; + static void staticMethod(); + }; + "#; + + let clang = ClangWrap::blocking_new().unwrap(); + let _ = clang.with_tu_from_string(&[], code, |tu| { + let classes = + classes_in_translation_unit(Rc::new(None), &tu, crate::MethodsToMockStrategy::All); + + assert_eq!(classes.len(), 1); + let class = &classes[0]; + assert_eq!(class.name, "MyClass"); + // staticMethod should be excluded + assert_eq!(class.methods.len(), 3); + + assert!(matches!( + &class.methods[0], + &MethodToMock { + name: ref n, + result_type: ref rt, + arguments: ref args, + is_const: true, + is_virtual: true, + is_noexcept: true, + ref_qualifier: None, + } + if n == "foo" && rt == "void" && args.is_empty() + )); + + assert!(matches!( + &class.methods[1], + &MethodToMock { + name: ref n, + result_type: ref rt, + arguments: ref args, + is_const: false, + is_virtual: false, + is_noexcept: false, + ref_qualifier: None, + } if n == "bar" + && rt == "int" + && args == &vec![Argument{ type_name: "int".to_string(), name: Some("x".to_string()) }] + )); + + assert!(matches!( + &class.methods[2], + &MethodToMock { + name: ref n, + result_type: ref rt, + arguments: ref args, + is_const: false, + is_virtual: true, + is_noexcept: false, + ref_qualifier: None, + } if n == "baz" && rt == "int" && args.is_empty() + )); + + Ok(()) + }); + } + + #[test] + fn unknown_arguments_types_can_be_handled() { + let code = r#" + class MyClass { + public: + virtual void foo(Unknown x) const noexcept; + void bar(Unknown); + static void staticMethods(Unknown); + }; + "#; + + let clang = ClangWrap::blocking_new().unwrap(); + let _ = clang.with_tu_from_string(&[], code, |tu| { + let classes = + classes_in_translation_unit(Rc::new(None), &tu, crate::MethodsToMockStrategy::All); + + assert_eq!(classes.len(), 1); + let class = &classes[0]; + assert_eq!(class.name, "MyClass"); + // staticMethod should be excluded + assert_eq!(class.methods.len(), 2); + + assert!(matches!( + &class.methods[0], + &MethodToMock { + name: ref n, + arguments: ref args, + is_const: true, + is_virtual: true, + is_noexcept: true, + .. + } + if n == "foo" && args == &vec![Argument { type_name: "Unknown".to_string(), name: Some("x".to_string()) }] + )); + + assert!(matches!( + &class.methods[1], + &MethodToMock { + name: ref n, + arguments: ref args, + is_const: false, + is_virtual: false, + is_noexcept: false, + .. + } + if n == "bar" && args == &vec![Argument { type_name: "Unknown".to_string(), name: None }] + )); + + Ok(()) + }); + } +} From 92872b771d89f890b096aac4da2001d55e9a217e Mon Sep 17 00:00:00 2001 From: Thomas Johannesson Date: Mon, 5 Jan 2026 13:31:16 +0100 Subject: [PATCH 08/13] SQ: trailing return type --- src/model.rs | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/model.rs b/src/model.rs index 4ee50ad..f61746b 100644 --- a/src/model.rs +++ b/src/model.rs @@ -243,6 +243,7 @@ mod tests { virtual void foo() const noexcept; int bar(int x); virtual int baz() = 0; + virtual auto bizz() const noexcept -> int = 0; static void staticMethod(); }; "#; @@ -256,7 +257,7 @@ mod tests { let class = &classes[0]; assert_eq!(class.name, "MyClass"); // staticMethod should be excluded - assert_eq!(class.methods.len(), 3); + assert_eq!(class.methods.len(), 4); assert!(matches!( &class.methods[0], @@ -300,6 +301,19 @@ mod tests { } if n == "baz" && rt == "int" && args.is_empty() )); + assert!(matches!( + &class.methods[3], + &MethodToMock { + name: ref n, + result_type: ref rt, + arguments: ref args, + is_const: true, + is_virtual: true, + is_noexcept: true, + ref_qualifier: None, + } if n == "bizz" && rt == "int" && args.is_empty() + )); + Ok(()) }); } From 5b662d9a61aabc16e3a8494461af079c3f519179 Mon Sep 17 00:00:00 2001 From: Thomas Johannesson Date: Mon, 5 Jan 2026 14:55:44 +0100 Subject: [PATCH 09/13] Fix argument type extraction of unknown type without name --- src/model.rs | 89 +++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 67 insertions(+), 22 deletions(-) diff --git a/src/model.rs b/src/model.rs index f61746b..59c7f8b 100644 --- a/src/model.rs +++ b/src/model.rs @@ -85,7 +85,7 @@ impl ModelFactory { .expect("Method should have arguments") .iter() .map(|arg| Argument { - type_name: self.get_type(arg), + type_name: self.get_argument_type(arg), name: arg.get_name(), }) .collect(), @@ -102,23 +102,53 @@ impl ModelFactory { } } - fn get_type(&mut self, entity: &clang::Entity) -> String { - self.extract_type_from_source(entity).unwrap_or_else(|| { - entity - .get_type() - .expect("Entity should have a type") - .get_display_name() - }) + fn get_argument_type(&mut self, arg_entity: &clang::Entity) -> String { + self.extract_argument_type_from_source(arg_entity) + .unwrap_or_else(|| { + arg_entity + .get_type() + .expect("Entity should have a type") + .get_display_name() + }) } - fn extract_type_from_source(&mut self, entity: &clang::Entity) -> Option { - if let Some(range) = entity.get_range() { - self.cache_file_contents(entity); + fn get_arg_range(&self, arg_entity: &clang::Entity) -> Option<(usize, usize)> { + // entity.get_range() only seems to work when argument has a name, but + // get_location() seems to work. We use it to find the start and then scan the + // source to find the end + if arg_entity.get_name().is_some() { + arg_entity.get_range().map(|r| { + let start = r.get_start().get_file_location().offset as usize; + let end = r.get_end().get_file_location().offset as usize; + (start, end) + }) + } else if let Some(file_contents) = &self.file_contents + && let Some(location) = arg_entity.get_location() + { + // Location is now _after_ the unknown argument type, so we need to scan + // backwards to find the start + let end = location.get_file_location().offset as usize; + let bytes = file_contents.as_bytes(); + let mut start = 0; + + for i in (0..end).rev() { + let c = bytes[i] as char; + if c == ',' || c == '(' { + start = i + 1; + break; + } + } + Some((start, end)) + } else { + None + } + } + fn extract_argument_type_from_source(&mut self, arg_entity: &clang::Entity) -> Option { + self.cache_file_contents(arg_entity); + if let Some((start, mut end)) = self.get_arg_range(arg_entity) { if let Some(file_contents) = &self.file_contents { - let start = range.get_start().get_file_location().offset as usize; - let mut end = range.get_end().get_file_location().offset as usize; - if let Some(name) = entity.get_name() { + if let Some(name) = arg_entity.get_name() { end -= name.len(); } @@ -127,7 +157,7 @@ impl ModelFactory { self.log, "Falling back to clang type extraction for entity {:?} \ due to illegal file position", - entity + arg_entity ); return None; } @@ -138,7 +168,7 @@ impl ModelFactory { self.log, "Falling back to clang type extraction for entity {:?} \ due to missing range or file contents", - entity + arg_entity ); None } @@ -315,30 +345,31 @@ mod tests { )); Ok(()) - }); + }).unwrap(); } #[test] - fn unknown_arguments_types_can_be_handled() { + fn unknown_argument_types_can_be_handled() { let code = r#" class MyClass { public: virtual void foo(Unknown x) const noexcept; void bar(Unknown); + void bizz(Unknown1, Unknown2 x, Unknown3); static void staticMethods(Unknown); }; "#; - let clang = ClangWrap::blocking_new().unwrap(); + let mut clang = ClangWrap::blocking_new().unwrap(); + clang.set_ignore_errors(true); let _ = clang.with_tu_from_string(&[], code, |tu| { let classes = classes_in_translation_unit(Rc::new(None), &tu, crate::MethodsToMockStrategy::All); assert_eq!(classes.len(), 1); let class = &classes[0]; - assert_eq!(class.name, "MyClass"); // staticMethod should be excluded - assert_eq!(class.methods.len(), 2); + assert_eq!(class.methods.len(), 3); assert!(matches!( &class.methods[0], @@ -366,7 +397,21 @@ mod tests { if n == "bar" && args == &vec![Argument { type_name: "Unknown".to_string(), name: None }] )); + assert!(matches!( + &class.methods[2], + &MethodToMock { + name: ref n, + arguments: ref args, + .. + } + if n == "bizz" && args == &vec![ + Argument { type_name: "Unknown1".to_string(), name: None }, + Argument { type_name: "Unknown2".to_string(), name: Some("x".to_string()) }, + Argument { type_name: "Unknown3".to_string(), name: None } + ] + )); + Ok(()) - }); + }).unwrap(); } } From 9a4ad63c4b3d1ba06b63e55a2314835ef6a0e111 Mon Sep 17 00:00:00 2001 From: Thomas Johannesson Date: Fri, 16 Jan 2026 15:56:01 +0100 Subject: [PATCH 10/13] SQ: clippy --- src/model.rs | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/src/model.rs b/src/model.rs index 59c7f8b..642f3ae 100644 --- a/src/model.rs +++ b/src/model.rs @@ -146,23 +146,23 @@ impl ModelFactory { fn extract_argument_type_from_source(&mut self, arg_entity: &clang::Entity) -> Option { self.cache_file_contents(arg_entity); - if let Some((start, mut end)) = self.get_arg_range(arg_entity) { - if let Some(file_contents) = &self.file_contents { - if let Some(name) = arg_entity.get_name() { - end -= name.len(); - } + if let Some((start, mut end)) = self.get_arg_range(arg_entity) + && let Some(file_contents) = &self.file_contents + { + if let Some(name) = arg_entity.get_name() { + end -= name.len(); + } - if start >= end || end > file_contents.len() { - log!( - self.log, - "Falling back to clang type extraction for entity {:?} \ + if start >= end || end > file_contents.len() { + log!( + self.log, + "Falling back to clang type extraction for entity {:?} \ due to illegal file position", - arg_entity - ); - return None; - } - return Some(file_contents[start..end].trim().to_string()); + arg_entity + ); + return None; } + return Some(file_contents[start..end].trim().to_string()); } log!( self.log, From f214f6718fe730d033cd29accd460ffbaf93f3df Mon Sep 17 00:00:00 2001 From: Thomas Johannesson Date: Fri, 16 Jan 2026 15:56:21 +0100 Subject: [PATCH 11/13] SQ: Move cache filling --- src/model.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/model.rs b/src/model.rs index 642f3ae..e1efd3b 100644 --- a/src/model.rs +++ b/src/model.rs @@ -57,6 +57,7 @@ impl ModelFactory { namespaces: &Vec, methods_to_mock: crate::MethodsToMockStrategy, ) -> ClassToMock { + self.cache_file_contents(class); ClassToMock { name: class.get_name().expect("Class should have a name"), namespaces: namespaces @@ -145,7 +146,6 @@ impl ModelFactory { } fn extract_argument_type_from_source(&mut self, arg_entity: &clang::Entity) -> Option { - self.cache_file_contents(arg_entity); if let Some((start, mut end)) = self.get_arg_range(arg_entity) && let Some(file_contents) = &self.file_contents { From c6ecdd14ee79e8cbe18bc37f849bf7704ae6e9ec Mon Sep 17 00:00:00 2001 From: Thomas Johannesson Date: Fri, 16 Jan 2026 16:00:23 +0100 Subject: [PATCH 12/13] Move ModelFactory to sub-module --- src/model.rs | 152 ++----------------------------------------- src/model/factory.rs | 149 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 153 insertions(+), 148 deletions(-) create mode 100644 src/model/factory.rs diff --git a/src/model.rs b/src/model.rs index e1efd3b..5716505 100644 --- a/src/model.rs +++ b/src/model.rs @@ -1,6 +1,8 @@ use crate::log; use std::rc::Rc; +mod factory; + // Represents a class that shall be mocked #[derive(Debug)] pub(crate) struct ClassToMock { @@ -37,156 +39,10 @@ pub(crate) fn classes_in_translation_unit( AstTraverser::new(log, root, methods_to_mock).traverse() } -// Factory for creating model objects from clang entities -struct ModelFactory { - log: Rc>, - file_contents: Option, -} - -impl ModelFactory { - fn new(log: Rc>) -> Self { - Self { - log, - file_contents: None, - } - } - - fn class_from_entity( - &mut self, - class: &clang::Entity, - namespaces: &Vec, - methods_to_mock: crate::MethodsToMockStrategy, - ) -> ClassToMock { - self.cache_file_contents(class); - ClassToMock { - name: class.get_name().expect("Class should have a name"), - namespaces: namespaces - .iter() - .map(|ns| ns.get_name().expect("Namespace should have a name")) - .collect::>(), - methods: class - .get_children() - .iter() - .filter(|child| child.get_kind() == clang::EntityKind::Method) - .filter(|method| methods_to_mock.should_mock(method)) - .map(|method| self.method_from_entity(method)) - .collect(), - } - } - - fn method_from_entity(&mut self, method: &clang::Entity) -> MethodToMock { - MethodToMock { - name: method.get_name().expect("Method should have a name"), - result_type: method - .get_result_type() - .expect("Method should have a return type") - .get_display_name(), - arguments: method - .get_arguments() - .expect("Method should have arguments") - .iter() - .map(|arg| Argument { - type_name: self.get_argument_type(arg), - name: arg.get_name(), - }) - .collect(), - is_const: method.is_const_method(), - is_virtual: method.is_virtual_method(), - is_noexcept: (method.get_exception_specification() - == Some(clang::ExceptionSpecification::BasicNoexcept)), - ref_qualifier: method.get_type().and_then(|t| t.get_ref_qualifier()).map( - |rq| match rq { - clang::RefQualifier::LValue => "&".to_string(), - clang::RefQualifier::RValue => "&&".to_string(), - }, - ), - } - } - - fn get_argument_type(&mut self, arg_entity: &clang::Entity) -> String { - self.extract_argument_type_from_source(arg_entity) - .unwrap_or_else(|| { - arg_entity - .get_type() - .expect("Entity should have a type") - .get_display_name() - }) - } - - fn get_arg_range(&self, arg_entity: &clang::Entity) -> Option<(usize, usize)> { - // entity.get_range() only seems to work when argument has a name, but - // get_location() seems to work. We use it to find the start and then scan the - // source to find the end - if arg_entity.get_name().is_some() { - arg_entity.get_range().map(|r| { - let start = r.get_start().get_file_location().offset as usize; - let end = r.get_end().get_file_location().offset as usize; - (start, end) - }) - } else if let Some(file_contents) = &self.file_contents - && let Some(location) = arg_entity.get_location() - { - // Location is now _after_ the unknown argument type, so we need to scan - // backwards to find the start - let end = location.get_file_location().offset as usize; - let bytes = file_contents.as_bytes(); - let mut start = 0; - - for i in (0..end).rev() { - let c = bytes[i] as char; - if c == ',' || c == '(' { - start = i + 1; - break; - } - } - Some((start, end)) - } else { - None - } - } - - fn extract_argument_type_from_source(&mut self, arg_entity: &clang::Entity) -> Option { - if let Some((start, mut end)) = self.get_arg_range(arg_entity) - && let Some(file_contents) = &self.file_contents - { - if let Some(name) = arg_entity.get_name() { - end -= name.len(); - } - - if start >= end || end > file_contents.len() { - log!( - self.log, - "Falling back to clang type extraction for entity {:?} \ - due to illegal file position", - arg_entity - ); - return None; - } - return Some(file_contents[start..end].trim().to_string()); - } - log!( - self.log, - "Falling back to clang type extraction for entity {:?} \ - due to missing range or file contents", - arg_entity - ); - None - } - - fn cache_file_contents(&mut self, entity: &clang::Entity) { - if self.file_contents.is_none() - && let Some(location) = entity.get_location() - && let Some(file) = location.get_file_location().file - { - self.file_contents = file.get_contents(); - } - } -} - // Traverses the AST to find classes to mock struct AstTraverser<'a> { root: clang::Entity<'a>, - factory: ModelFactory, + factory: factory::ModelFactory, methods_to_mock: crate::MethodsToMockStrategy, classes: Vec, @@ -201,7 +57,7 @@ impl<'a> AstTraverser<'a> { ) -> Self { Self { root: root.get_entity(), - factory: ModelFactory::new(log), + factory: factory::ModelFactory::new(log), methods_to_mock, classes: Vec::new(), namespace_stack: Vec::new(), diff --git a/src/model/factory.rs b/src/model/factory.rs new file mode 100644 index 0000000..c90c192 --- /dev/null +++ b/src/model/factory.rs @@ -0,0 +1,149 @@ +use super::{Argument, ClassToMock, MethodToMock}; +use crate::log; +use std::rc::Rc; + +// Factory for creating model objects from clang entities +pub(crate) struct ModelFactory { + log: Rc>, + file_contents: Option, +} + +impl ModelFactory { + pub(crate) fn new(log: Rc>) -> Self { + Self { + log, + file_contents: None, + } + } + + pub(crate) fn class_from_entity( + &mut self, + class: &clang::Entity, + namespaces: &Vec, + methods_to_mock: crate::MethodsToMockStrategy, + ) -> ClassToMock { + self.cache_file_contents(class); + ClassToMock { + name: class.get_name().expect("Class should have a name"), + namespaces: namespaces + .iter() + .map(|ns| ns.get_name().expect("Namespace should have a name")) + .collect::>(), + methods: class + .get_children() + .iter() + .filter(|child| child.get_kind() == clang::EntityKind::Method) + .filter(|method| methods_to_mock.should_mock(method)) + .map(|method| self.method_from_entity(method)) + .collect(), + } + } + + fn method_from_entity(&mut self, method: &clang::Entity) -> MethodToMock { + MethodToMock { + name: method.get_name().expect("Method should have a name"), + result_type: method + .get_result_type() + .expect("Method should have a return type") + .get_display_name(), + arguments: method + .get_arguments() + .expect("Method should have arguments") + .iter() + .map(|arg| Argument { + type_name: self.get_argument_type(arg), + name: arg.get_name(), + }) + .collect(), + is_const: method.is_const_method(), + is_virtual: method.is_virtual_method(), + is_noexcept: (method.get_exception_specification() + == Some(clang::ExceptionSpecification::BasicNoexcept)), + ref_qualifier: method.get_type().and_then(|t| t.get_ref_qualifier()).map( + |rq| match rq { + clang::RefQualifier::LValue => "&".to_string(), + clang::RefQualifier::RValue => "&&".to_string(), + }, + ), + } + } + + fn get_argument_type(&mut self, arg_entity: &clang::Entity) -> String { + self.extract_argument_type_from_source(arg_entity) + .unwrap_or_else(|| { + arg_entity + .get_type() + .expect("Entity should have a type") + .get_display_name() + }) + } + + fn get_arg_range(&self, arg_entity: &clang::Entity) -> Option<(usize, usize)> { + // entity.get_range() only seems to work when argument has a name, but + // get_location() seems to work. We use it to find the start and then scan the + // source to find the end + if arg_entity.get_name().is_some() { + arg_entity.get_range().map(|r| { + let start = r.get_start().get_file_location().offset as usize; + let end = r.get_end().get_file_location().offset as usize; + (start, end) + }) + } else if let Some(file_contents) = &self.file_contents + && let Some(location) = arg_entity.get_location() + { + // Location is now _after_ the unknown argument type, so we need to scan + // backwards to find the start + let end = location.get_file_location().offset as usize; + let bytes = file_contents.as_bytes(); + let mut start = 0; + + for i in (0..end).rev() { + let c = bytes[i] as char; + if c == ',' || c == '(' { + start = i + 1; + break; + } + } + Some((start, end)) + } else { + None + } + } + + fn extract_argument_type_from_source(&mut self, arg_entity: &clang::Entity) -> Option { + if let Some((start, mut end)) = self.get_arg_range(arg_entity) + && let Some(file_contents) = &self.file_contents + { + if let Some(name) = arg_entity.get_name() { + end -= name.len(); + } + + if start >= end || end > file_contents.len() { + log!( + self.log, + "Falling back to clang type extraction for entity {:?} \ + due to illegal file position", + arg_entity + ); + return None; + } + return Some(file_contents[start..end].trim().to_string()); + } + log!( + self.log, + "Falling back to clang type extraction for entity {:?} \ + due to missing range or file contents", + arg_entity + ); + None + } + + fn cache_file_contents(&mut self, entity: &clang::Entity) { + if self.file_contents.is_none() + && let Some(location) = entity.get_location() + && let Some(file) = location.get_file_location().file + { + self.file_contents = file.get_contents(); + } + } +} From ed0b379fba0d872f40b5162141b5414fd5050c10 Mon Sep 17 00:00:00 2001 From: Thomas Johannesson Date: Fri, 16 Jan 2026 16:57:57 +0100 Subject: [PATCH 13/13] WIP: Parse declarations TODO: - should_mock() must use MethodToMock to take all extras into account. - Tests of behavior of entity.is_virtual, is_pure_virtual and is_static etc when return type is unknown - Tests of signature parsing - Tests... --- src/model.rs | 11 ++++--- src/model/factory.rs | 76 +++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 82 insertions(+), 5 deletions(-) diff --git a/src/model.rs b/src/model.rs index 5716505..d6caf05 100644 --- a/src/model.rs +++ b/src/model.rs @@ -17,8 +17,10 @@ pub(crate) struct MethodToMock { pub(crate) name: String, pub(crate) result_type: String, pub(crate) arguments: Vec, + is_static: bool, pub(crate) is_const: bool, pub(crate) is_virtual: bool, + is_pure_virtual: bool, pub(crate) is_noexcept: bool, pub(crate) ref_qualifier: Option, } @@ -107,11 +109,12 @@ impl<'a> AstTraverser<'a> { } impl crate::MethodsToMockStrategy { - fn should_mock(self, method: &clang::Entity) -> bool { + // TODO: Must look at MethodToMock rather than clang::Entity! + fn should_mock(self, method: &MethodToMock) -> bool { match self { - crate::MethodsToMockStrategy::All => !method.is_static_method(), - crate::MethodsToMockStrategy::AllVirtual => method.is_virtual_method(), - crate::MethodsToMockStrategy::OnlyPureVirtual => method.is_pure_virtual_method(), + crate::MethodsToMockStrategy::All => !method.is_static, + crate::MethodsToMockStrategy::AllVirtual => method.is_virtual, + crate::MethodsToMockStrategy::OnlyPureVirtual => method.is_pure_virtual, } } } diff --git a/src/model/factory.rs b/src/model/factory.rs index c90c192..3b1fe8a 100644 --- a/src/model/factory.rs +++ b/src/model/factory.rs @@ -8,6 +8,13 @@ pub(crate) struct ModelFactory { file_contents: Option, } +// Represents signature details parsed from source code method declaration +struct MethodSignature { + is_virtual: bool, + is_pure_virtual: bool, + is_static: bool, +} + impl ModelFactory { pub(crate) fn new(log: Rc>) -> Self { Self { @@ -40,6 +47,11 @@ impl ModelFactory { } fn method_from_entity(&mut self, method: &clang::Entity) -> MethodToMock { + println!("Processing method: {:?}", method); + let signature = self + .extract_method_declaration_from_source(method) + .map_or(None, |d| MethodSignature::parse_declaration(&d)); + MethodToMock { name: method.get_name().expect("Method should have a name"), result_type: method @@ -55,8 +67,13 @@ impl ModelFactory { name: arg.get_name(), }) .collect(), + is_static: method.is_static_method() + || signature.as_ref().map_or(false, |s| s.is_static), is_const: method.is_const_method(), - is_virtual: method.is_virtual_method(), + is_virtual: method.is_virtual_method() + || signature.as_ref().map_or(false, |s| s.is_virtual), + is_pure_virtual: method.is_pure_virtual_method() + || signature.as_ref().map_or(false, |s| s.is_pure_virtual), is_noexcept: (method.get_exception_specification() == Some(clang::ExceptionSpecification::BasicNoexcept)), ref_qualifier: method.get_type().and_then(|t| t.get_ref_qualifier()).map( @@ -78,6 +95,17 @@ impl ModelFactory { }) } + fn get_method_declaration_range( + &self, + method_entity: &clang::Entity, + ) -> Option<(usize, usize)> { + method_entity.get_range().map(|r| { + let start = r.get_start().get_file_location().offset as usize; + let end = r.get_end().get_file_location().offset as usize; + (start, end) + }) + } + fn get_arg_range(&self, arg_entity: &clang::Entity) -> Option<(usize, usize)> { // entity.get_range() only seems to work when argument has a name, but // get_location() seems to work. We use it to find the start and then scan the @@ -110,6 +138,15 @@ impl ModelFactory { } } + fn extract_method_declaration_from_source(&mut self, method: &clang::Entity) -> Option { + if let Some((start, end)) = self.get_method_declaration_range(method) + && let Some(file_contents) = &self.file_contents + { + return Some(file_contents[start..end].trim().to_string()); + } + None + } + fn extract_argument_type_from_source(&mut self, arg_entity: &clang::Entity) -> Option { if let Some((start, mut end)) = self.get_arg_range(arg_entity) && let Some(file_contents) = &self.file_contents @@ -147,3 +184,40 @@ impl ModelFactory { } } } + +impl MethodSignature { + fn parse_declaration(decl: &str) -> Option { + // Remove part not part of signature, e.g., function body + let signature = decl.split(";").next().unwrap().split("{").next().unwrap(); + println!("\nSignature: {:#?}", signature); + + let pre_parts = signature + .split("(") + .next() + .unwrap() + .split_ascii_whitespace() + .collect::>(); + let Some(post_parts) = signature + .split(")") + .skip(1) + .next() + .map(|s| s.split_ascii_whitespace().collect::>()) + else { + return None; + }; + //println!("Post parts: {:#?}", post_parts); + + let is_virtual = pre_parts.iter().any(|s| *s == "virtual") + || post_parts.iter().any(|s| *s == "override"); + let is_pure_virtual = is_virtual + && (post_parts.iter().any(|s| *s == "=0") + || post_parts.windows(2).any(|w| w[0] == "=" && w[1] == "0")); + let is_static = pre_parts.iter().any(|s| *s == "static"); + + Some(MethodSignature { + is_virtual, + is_pure_virtual, + is_static, + }) + } +}