diff --git a/crates/ruff_cpp_spo/examples/harvest_events.rs b/crates/ruff_cpp_spo/examples/harvest_events.rs index 05b55c4ce90c06..d69cc85bb73596 100644 --- a/crates/ruff_cpp_spo/examples/harvest_events.rs +++ b/crates/ruff_cpp_spo/examples/harvest_events.rs @@ -33,23 +33,53 @@ use std::path::PathBuf; use ruff_cpp_spo::events::{EventKind, MethodOre, Symbols, walk_tu_events}; -/// A TSV cell can carry no tab and no newline. C++ identifiers and type -/// spellings contain neither, but a malformed one would silently shift every -/// later column, so the guard is unconditional. +/// Replaces tab and line-break characters in a TSV cell with spaces. +/// +/// # Examples +/// +/// ``` +/// assert_eq!(cell("a\tb\nc\rd"), "a b c d"); +/// ``` fn cell(s: &str) -> String { s.replace(['\t', '\n', '\r'], " ") } -/// Pull the compile flags out of one `compile_commands.json` entry. Only the -/// flags that change what the parser SEES are kept — `-o`, `-c` and the input -/// file itself would make libclang re-drive a compilation. +/// Extracts parser-relevant flags from a compilation command. +/// +/// Compilation and output options, along with the source file, are omitted. +/// +/// # Examples +/// +/// ``` +/// let flags = args_of("clang++ -Iinclude -std=c++17 -c main.cpp -o main.o"); +/// assert_eq!(flags, vec!["-Iinclude", "-std=c++17"]); +/// ``` fn args_of(cmd: &str) -> Vec { keep_parse_flags(&shell_split(cmd)) } -/// The filter half of [`args_of`], over tokens that are ALREADY split. The -/// `arguments` form of a compilation database is a real array, so re-joining it -/// into a string just to re-split it could only lose information. +/// Filters compiler arguments to retain parser-relevant include, macro, and language-standard flags. +/// +/// # Examples +/// +/// ``` +/// let args = vec![ +/// "-I".to_owned(), +/// "include".to_owned(), +/// "-DDEBUG".to_owned(), +/// "-std=c++17".to_owned(), +/// "-o".to_owned(), +/// "output.o".to_owned(), +/// ]; +/// +/// assert_eq!( +/// keep_parse_flags(&args), +/// vec!["-I", "include", "-DDEBUG", "-std=c++17"] +/// ); +/// ``` +/// +/// The returned arguments include `-I`, `-isystem`, and `-include` together +/// with their following values, as well as joined `-I`, `-D`, and `-std` flags. fn keep_parse_flags(toks: &[String]) -> Vec { let mut out = Vec::new(); let mut i = 0; @@ -74,7 +104,16 @@ fn keep_parse_flags(toks: &[String]) -> Vec { out } -/// Minimal POSIX-ish splitter: enough for the quoting cmake emits. +/// Splits a command string into whitespace-separated arguments while honoring single quotes, double quotes, and backslash escapes. +/// +/// # Examples +/// +/// ``` +/// assert_eq!( +/// shell_split(r#"clang++ -I"include dir" 'source file.cpp'"#), +/// vec!["clang++", "-Iinclude dir", "source file.cpp"] +/// ); +/// ``` fn shell_split(s: &str) -> Vec { let mut out = Vec::new(); let mut cur = String::new(); @@ -108,20 +147,22 @@ fn shell_split(s: &str) -> Vec { out } -/// `compile_commands.json` without a JSON dependency: the file is a flat array -/// of objects, conventionally pretty-printed one field per line, which is the -/// shape this reads. +/// Parses compilation database entries into source paths and parser-relevant arguments. +/// +/// Supports both shell-escaped `command` strings and already-split `arguments` +/// arrays. Entries without either command form are ignored and reported. /// -/// Each object is parsed INDEPENDENTLY and flushed at its closing brace. An -/// earlier version kept `file` and `command` across objects and emitted as soon -/// as both were set; an entry using `arguments` therefore left its `file` -/// pending, and the NEXT object's `command` was attributed to the wrong file. +/// # Examples /// -/// Both command forms defined by the JSON compilation database are read: -/// `"command"` (one shell-escaped string) and `"arguments"` (an array of -/// already-split strings, which the spec prefers precisely because it needs no -/// shell unescaping). An entry carrying neither is reported rather than -/// silently dropped. +/// ``` +/// let entries = parse_cc_json( +/// r#"[{"file":"src/main.cpp","arguments":["clang++","-I","include","-c","src/main.cpp"]}]"# +/// ); +/// +/// assert_eq!(entries.len(), 1); +/// assert_eq!(entries[0].0, std::path::PathBuf::from("src/main.cpp")); +/// assert_eq!(entries[0].1, vec!["-I", "include"]); +/// ``` fn parse_cc_json(text: &str) -> Vec<(PathBuf, Vec)> { let mut out = Vec::new(); let mut file: Option = None; @@ -183,19 +224,15 @@ fn parse_cc_json(text: &str) -> Vec<(PathBuf, Vec)> { out } -/// Every quoted run in a JSON array of strings, in order. +/// Finds the first closing bracket outside a quoted JSON string. /// -/// Scans for unescaped `"` delimiters rather than splitting on `,`, because a -/// comma inside a string is CONTENT: `["-DPAIR=std::pair"]` is one -/// argument, and a comma split turns it into two invalid fragments that are -/// then dropped, silently changing the flags a translation unit is parsed with. -/// The byte index of the first `]` that is NOT inside a JSON string, or `None` -/// when the array is still open. +/// # Examples /// -/// Shares its string/escape state machine with [`json_string_array`], so the -/// two agree on where a string starts and ends; a scanner that disagreed with -/// the one doing the extraction would be a second source of truth about the -/// same text. +/// ``` +/// assert_eq!(unquoted_bracket(r#"["a]"]]"#), Some(5)); +/// assert_eq!(unquoted_bracket(r#"["a"]"#), None); +/// ``` +fn unquoted_bracket(text: &str) -> Option { fn unquoted_bracket(text: &str) -> Option { let (mut inside, mut esc) = (false, false); for (i, c) in text.char_indices() { @@ -216,6 +253,17 @@ fn unquoted_bracket(text: &str) -> Option { None } +/// Extracts quoted string values from text and partially unescapes newline and tab escapes. +/// +/// # Examples +/// +/// ``` +/// let values = json_string_array(r#"[ "one", "two\n" ]"#); +/// assert_eq!(values, vec!["one", "two\n"]); +/// ``` +/// +/// @param text Text containing quoted string values. +/// @returns The extracted string values in their order of appearance. fn json_string_array(text: &str) -> Vec { let mut out = Vec::new(); let mut cur = String::new(); @@ -247,6 +295,19 @@ fn json_string_array(text: &str) -> Vec { out } +/// Extracts a quoted string value following a matching field prefix. +/// +/// Recognizes `\n` and `\t` escapes and returns `None` when the prefix or +/// quoted value is missing. +/// +/// # Examples +/// +/// ``` +/// assert_eq!( +/// json_string_field(r#""name": "line\nvalue""#, r#""name":"#), +/// Some("line\nvalue".to_string()) +/// ); +/// ``` fn json_string_field(line: &str, key: &str) -> Option { let rest = line.strip_prefix(key)?.trim_start(); let body = rest.strip_prefix('"')?; @@ -271,6 +332,17 @@ fn json_string_field(line: &str, key: &str) -> Option { None } +/// Harvests translation-unit events and writes event, scope, method, and symbol TSV files. +/// +/// The corpus is selected with `ORE_CC_JSON` or `ORE_FILE`; `ORE_OUT` selects the +/// output directory. Returns an error when no corpus is selected or no translation +/// units remain after filtering. +/// +/// # Examples +/// +/// ```text +/// ORE_CC_JSON=compile_commands.json ORE_OUT=/tmp/ore cargo run --example harvest_events +/// ``` fn main() -> Result<(), Box> { let out_dir = PathBuf::from(std::env::var("ORE_OUT").unwrap_or_else(|_| "/tmp/ore".into())); std::fs::create_dir_all(&out_dir)?; diff --git a/crates/ruff_cpp_spo/examples/harvest_ladybug.rs b/crates/ruff_cpp_spo/examples/harvest_ladybug.rs index b2e60718bc6d72..44e9a51354be71 100644 --- a/crates/ruff_cpp_spo/examples/harvest_ladybug.rs +++ b/crates/ruff_cpp_spo/examples/harvest_ladybug.rs @@ -59,6 +59,19 @@ use std::path::{Path, PathBuf}; use ruff_cpp_spo::{CppClass, Declaration, NAMESPACE, model_from_class, walk_tu_with_diagnostics}; use ruff_spo_triplet::{ModelGraph, RecipeCentroid, classify, expand, to_ndjson}; +/// Recursively collects regular `.h` files beneath a directory, skipping symlinks. +/// +/// # Examples +/// +/// ``` +/// use std::path::Path; +/// +/// let mut headers = Vec::new(); +/// collect_headers(Path::new("src/include"), &mut headers)?; +/// # Ok::<(), std::io::Error>(()) +/// ``` +/// +/// Returns an error if a directory entry cannot be read or traversed. fn collect_headers(dir: &Path, out: &mut Vec) -> std::io::Result<()> { for entry in std::fs::read_dir(dir)? { let entry = entry?; @@ -81,6 +94,24 @@ fn collect_headers(dir: &Path, out: &mut Vec) -> std::io::Result<()> { Ok(()) } +/// Harvests C++ class metadata from the configured Ladybug include subtree and writes an NDJSON SPO manifest. +/// +/// The source root, subtree, and manifest path are configured with `LADYBUG_SRC`, `SUBTREE`, and +/// `MANIFEST_OUT`. Missing or unreadable input directories and manifest-write failures are reported +/// as errors; individual header parse failures are collected and reported without aborting the +/// harvest. +/// +/// # Examples +/// +/// ```no_run +/// let result = main(); +/// assert!(result.is_ok()); +/// ``` +/// +/// # Errors +/// +/// Returns an error if the configured subtree does not exist, header traversal fails, or the +/// manifest cannot be written. fn main() -> Result<(), Box> { let root = std::env::var("LADYBUG_SRC").unwrap_or_else(|_| "/home/user/ladybug".to_string()); let root = Path::new(&root); diff --git a/crates/ruff_cpp_spo/src/clang_walker.rs b/crates/ruff_cpp_spo/src/clang_walker.rs index 93619bad9ca254..818e1780cf68a9 100644 --- a/crates/ruff_cpp_spo/src/clang_walker.rs +++ b/crates/ruff_cpp_spo/src/clang_walker.rs @@ -106,25 +106,27 @@ impl fmt::Display for ParseDiagnostic { } } -/// Like [`walk_tu`], but also returns every libclang parse diagnostic at -/// [`Severity::Error`] or higher (one parse, not two — [`walk_tu`] is a thin -/// wrapper over this). -/// -/// `walk_tu`'s `Ok` alone can mislead a caller into treating "the parse -/// returned `Ok`, 0 failed" as "the whole TU was captured". libclang -/// recovers from an unresolved `#include` by treating the file as -/// successfully parsed while simply DROPPING the incomplete declaration that -/// needed the missing header — no `Err`, no partial-class marker, nothing on -/// [`CppClass`] hints at the gap. This is the exact `STATS`/`scrollview.h` -/// gap found harvesting Tesseract (`statistc.h` includes `scrollview.h` for -/// GRAPHICS_DISABLED-gated declarations; without `src/viewer` on the include -/// path, `STATS`'s `CXXRecordDecl` silently never completes and the class is -/// simply ABSENT from `walk_tu`'s output — -/// `tesseract-rs/.claude/harvest/statistc-manifest.txt`). A caller doing a -/// multi-header sweep (see `examples/harvest_textord.rs`) should call this -/// instead of `walk_tu` and warn loudly when the returned diagnostic list is -/// non-empty — "0 failed" from `walk_tu` alone does NOT mean the sweep is -/// complete. +/// Parses a translation unit and returns extracted classes together with error-level +/// and higher libclang diagnostics from the same parse. +/// +/// Parse diagnostics can reveal incomplete extraction caused by issues such as +/// unresolved includes, even when parsing succeeds. +/// +/// # Examples +/// +/// ```no_run +/// let (classes, diagnostics) = walk_tu_with_diagnostics( +/// std::path::Path::new("input.hpp"), +/// &[], +/// )?; +/// +/// if !diagnostics.is_empty() { +/// eprintln!("{} parse diagnostics", diagnostics.len()); +/// } +/// # let _ = classes; +/// # Ok::<(), WalkError>(()) +/// ``` +pub fn walk_tu_with_diagnostics( pub fn walk_tu_with_diagnostics( path: &Path, args: &[String], @@ -132,23 +134,32 @@ pub fn walk_tu_with_diagnostics( walk_tu_configured(path, args, Some(&BodyArmConfig::default())) } -/// [`walk_tu_with_diagnostics`] with explicit control over the body arm. +/// Extracts C++ class declarations and parse diagnostics, optionally collecting method-body facts. /// -/// `arm` decides BOTH what is harvested and how the TU is parsed: -/// -/// - `Some(cfg)` — parse WITH function bodies and fill each [`CppMethod`]'s -/// `writes` / `reads` / `raises` / `calls` / `guarded_writes` from the body, -/// using `cfg`'s mutator vocabulary. This is what [`walk_tu`] and -/// [`walk_tu_with_diagnostics`] do. -/// - `None` — skip function bodies (a faster parse) and leave the arm empty. -/// The signature plane is identical either way, so a consumer that only -/// reconstructs declarations (`ruff_cpp_codegen`) loses nothing by asking -/// for this. +/// When `arm` is `Some`, function bodies are parsed and method writes, reads, raised +/// exceptions, configured calls, and guarded writes are collected according to the +/// supplied configuration. When `arm` is `None`, bodies are skipped and only +/// declaration information is extracted. /// /// # Errors /// -/// [`WalkError::Libclang`] if libclang fails to initialise; -/// [`WalkError::Parse`] if the TU fails to parse. +/// Returns [`WalkError::Libclang`] if libclang cannot be initialized, or +/// [`WalkError::Parse`] if the translation unit cannot be parsed. +/// +/// # Examples +/// +/// ```no_run +/// use std::path::Path; +/// +/// let args = vec!["-std=c++17".to_owned()]; +/// let (classes, diagnostics) = walk_tu_configured( +/// Path::new("header.hpp"), +/// &args, +/// None, +/// )?; +/// # let _: (Vec, Vec) = (classes, diagnostics); +/// # Ok::<(), WalkError>(()) +/// ``` pub fn walk_tu_configured( path: &Path, args: &[String], @@ -516,8 +527,16 @@ fn tally_class_bodies(entity: &Entity, hist: &mut BTreeMap) { } } -/// Recurse the AST, emitting a [`CppClass`] for every class/struct -/// definition (recursing into namespaces and nested classes). +/// Collects project-defined class and struct definitions from an AST subtree, including nested classes. +/// +/// Classes from system headers are excluded, while namespaces and nested class declarations are traversed recursively. +/// +/// # Examples +/// +/// ```ignore +/// let mut classes = Vec::new(); +/// collect_classes(&entity, &mut classes, None); +/// ``` fn collect_classes(entity: &Entity, out: &mut Vec, arm: Option<&BodyArmConfig>) { for child in entity.get_children() { match child.get_kind() { @@ -551,9 +570,16 @@ fn collect_classes(entity: &Entity, out: &mut Vec, arm: Option<&BodyAr } } -/// Build a [`CppClass`] from a class/struct definition cursor by reading its -/// DIRECT member children (bases, fields, methods). Nested class decls are -/// ignored here — [`collect_classes`] emits them separately. +/// Builds a [`CppClass`] from a class or struct definition cursor. +/// +/// The class includes direct bases, fields, member functions, friends, and nested +/// enums. Nested class declarations are collected separately by [`collect_classes`]. +/// +/// # Examples +/// +/// ```rust,ignore +/// let class = build_class(&class_cursor, None); +/// ``` fn build_class(e: &Entity, arm: Option<&BodyArmConfig>) -> Option { // A `ClassTemplatePartialSpecialization` shares its primary's `get_name()` // (libclang spells it as the bare template name, e.g. `Foo` for @@ -713,9 +739,16 @@ fn in_system_header(e: &Entity) -> bool { .is_some_and(|loc| loc.is_in_system_header()) } -/// The enclosing named scopes of `e` (namespaces + outer classes), -/// outermost first — the [`CppClass::namespace`] components. The class's -/// own name is excluded. +/// Returns the enclosing named namespaces and outer classes for an entity, ordered from outermost to innermost. +/// +/// The entity's own name is excluded. +/// +/// # Examples +/// +/// ```ignore +/// let scopes = enclosing_scopes(&entity); +/// assert_eq!(scopes, vec!["outer".to_string(), "Inner".to_string()]); +/// ``` fn enclosing_scopes(e: &Entity) -> Vec { let mut parts = Vec::new(); let mut cur = e.get_semantic_parent(); @@ -734,7 +767,14 @@ fn enclosing_scopes(e: &Entity) -> Vec { parts } -/// The fully-qualified name of a class-like cursor (`Namespace::Outer::Name`). +/// Builds the fully qualified name of a class-like entity from its enclosing scopes. +/// +/// # Examples +/// +/// ```rust,ignore +/// let name = qualified_name(&entity); +/// assert_eq!(name, "Namespace::Outer::Name"); +/// ``` pub(crate) fn qualified_name(e: &Entity) -> String { let mut parts = enclosing_scopes(e); if let Some(n) = e.get_name() { @@ -743,6 +783,22 @@ pub(crate) fn qualified_name(e: &Entity) -> String { parts.join("::") } +/// Builds a base-class record from a Clang base-specifier entity. +/// +/// Resolves the base's qualified name when available, preserves its access +/// and virtual-inheritance attributes, and falls back to the displayed type +/// name for unresolved bases. +/// +/// # Examples +/// +/// ```ignore +/// let base = build_base(&base_entity); +/// assert!(base.is_some()); +/// ``` +/// +/// # Returns +/// +/// `Some(CppBase)` when the entity has a type, or `None` otherwise. fn build_base(m: &Entity) -> Option { let ty = m.get_type()?; // Prefer the resolved declaration's qualified name; fall back to the @@ -765,11 +821,19 @@ fn build_base(m: &Entity) -> Option { }) } -/// Build a [`CppMethod`] from a member-function cursor. `arm` is `Some` when -/// the translation unit was parsed WITH bodies, in which case the body arm is -/// harvested from the method's DEFINITION — which for a method declared in a -/// header and defined in a `.cpp` is a different cursor from `m`, and is the -/// only one that has a body to read. +/// Builds a method model from a member-function cursor, including its signature, +/// override target, qualifiers, access, and body facts when available. +/// +/// `arm` enables body extraction from the method definition when the translation +/// unit was parsed with function bodies. +/// +/// # Examples +/// +/// ```ignore +/// let method = build_method(&member_cursor, Some(&BodyArmConfig::default())); +/// assert_eq!(method.name, "process"); +/// ``` +fn build_method doc? They demand docstring itself, should include /// only, not signature. But example references vars and private types; ignore okay. Must summary not starts Returns. Fine. However output should not include fn. done. fn build_method(m: &Entity, arm: Option<&BodyArmConfig>) -> CppMethod { let name = m.get_name().unwrap_or_default(); let is_noexcept = matches!( @@ -941,6 +1005,15 @@ pub struct BodyArmConfig { #[cfg(feature = "libclang")] impl Default for BodyArmConfig { + /// Creates a body-arm configuration with the standard lifecycle mutator names. + /// + /// # Examples + /// + /// ``` + /// let config = BodyArmConfig::default(); + /// assert!(config.mutators.contains(&"save".to_string())); + /// assert!(config.mutator_prefixes.is_empty()); + /// ``` fn default() -> Self { Self { mutators: [ @@ -958,22 +1031,47 @@ impl Default for BodyArmConfig { #[cfg(feature = "libclang")] impl BodyArmConfig { - /// Replace the exact-match mutator set. - #[must_use] + /// Replaces the exact-match mutator names used for body-arm analysis. + /// + /// # Examples + /// + /// ``` + /// let config = BodyArmConfig::default() + /// .with_mutators([String::from("reset"), String::from("clear")]); + /// ``` pub fn with_mutators(mut self, names: impl IntoIterator) -> Self { self.mutators = names.into_iter().collect(); self } - /// Add prefixes that make any method name starting with one a mutator - /// (e.g. `"Set"` to treat every `SetFoo` as a writer). + /// Configures method-name prefixes that identify mutator calls. + /// + /// # Examples + /// + /// ``` + /// let config = BodyArmConfig::default() + /// .with_mutator_prefixes([String::from("Set"), String::from("Reset")]); + /// ``` #[must_use] pub fn with_mutator_prefixes(mut self, prefixes: impl IntoIterator) -> Self { self.mutator_prefixes = prefixes.into_iter().collect(); self } - /// Does a call to `name` count as a lifecycle mutator? + /// Determines whether a method name is configured as a lifecycle mutator. + /// + /// # Examples + /// + /// ``` + /// let config = BodyArmConfig { + /// mutators: vec!["reset".to_string()], + /// mutator_prefixes: vec!["set_".to_string()], + /// }; + /// + /// assert!(config.is_mutator("reset")); + /// assert!(config.is_mutator("set_value")); + /// assert!(!config.is_mutator("read")); + /// ``` #[must_use] pub fn is_mutator(&self, name: &str) -> bool { self.mutators.iter().any(|m| m == name) @@ -1011,15 +1109,25 @@ enum GuardedBranch { Else, } -/// Extract the recipe fingerprint from a member function's body. +/// Collects member-access, exception, call, and guarded-write facts from a C++ method. /// -/// Call with the entity that HAS the body — [`Entity::get_definition`] where -/// the definition is out of line, the declaration cursor where it is inline. -/// Guard detection is deliberately local (an `IfStmt` whose condition is an -/// absence test on member `X`, threaded into the branch that writes `X`) — no -/// dominator analysis, which is what keeps `writes_if_blank` Authoritative, -/// exactly as the Ruby `detect_guarded_default` does. -#[cfg(feature = "libclang")] +/// Pass the entity that owns the body: use the definition cursor for an +/// out-of-line method and the declaration cursor for an inline method. The +/// returned facts are deduplicated. Signature-only entities produce an empty +/// result. +/// +/// # Examples +/// +/// ``` +/// # fn example(method: &Entity, cfg: &BodyArmConfig) { +/// let facts = method_body_arm(method, cfg); +/// # let _ = facts; +/// # } +/// ``` +/// +/// `cfg` controls which receiver-method calls are recorded. +/// +/// Returns the extracted body facts. pub(crate) fn method_body_arm(method: &Entity, cfg: &BodyArmConfig) -> BodyArm { let mut arm = BodyArm::default(); // Parameters are skipped rather than selecting the CompoundStmt, because a @@ -1052,20 +1160,28 @@ pub(crate) fn method_body_arm(method: &Entity, cfg: &BodyArmConfig) -> BodyArm { arm } -/// Walk every CHILD of `node`. `guard` is the member the enclosing branch is -/// absence-guarded on (J1), threaded down only into that branch. -#[cfg(feature = "libclang")] +/// Traverses the child entities of a body-analysis node while preserving the active absence guard. +/// +/// # Examples +/// +/// ```ignore +/// walk_body(&node, &mut arm, &config, None); +/// ``` fn walk_body(node: &Entity, arm: &mut BodyArm, cfg: &BodyArmConfig, guard: Option<&str>) { for child in node.get_children() { walk_node(&child, arm, cfg, guard); } } -/// The LHS of an assignment, minus the member reference that IS the target. +/// Traverses an assignment's left-hand side while recording the target only as a write. /// -/// `arr_[i] = v` must record the subscript `i` as a read and `arr_` only as a -/// write; the naive child walk records `arr_` twice, once through each role. -#[cfg(feature = "libclang")] +/// Index expressions and other non-target subexpressions are still analyzed for reads. +/// +/// # Examples +/// +/// ```ignore +/// walk_lhs_skipping_target(&lhs, &mut arm, &cfg, None); +/// ``` fn walk_lhs_skipping_target( lhs: &Entity, arm: &mut BodyArm, @@ -1085,14 +1201,16 @@ fn walk_lhs_skipping_target( } } -/// Walk everything under the write target EXCEPT the target's own reference. +/// Walks the expressions that qualify an assignment target and its subscript indices. +/// +/// This records reads from qualifying objects and indices while excluding the target +/// member reference itself. /// -/// Descending with `walk_body` is not enough: the reference arrives wrapped in -/// an `UnexposedExpr`, so walking the wrapper's children lands straight back on -/// the `MemberRefExpr` and records the read this exists to avoid. Follow the -/// same descent `assignment_target` uses, and walk only what QUALIFIES the -/// member (`this`, or `p` in `p.x_`) plus any subscript indices passed on the -/// way down. +/// # Examples +/// +/// ```ignore +/// walk_past_target(&target, &mut arm, &config, None); +/// ``` #[cfg(feature = "libclang")] fn walk_past_target(e: &Entity, arm: &mut BodyArm, cfg: &BodyArmConfig, guard: Option<&str>) { let mut cur = *e; @@ -1125,8 +1243,17 @@ fn walk_past_target(e: &Entity, arm: &mut BodyArm, cfg: &BodyArmConfig, guard: O } } -/// Walk ONE node, matching it before descending. -#[cfg(feature = "libclang")] +/// Analyzes an AST node and its descendants, recording body facts in `arm`. +/// +/// The optional `guard` associates guarded writes with the condition that protects +/// them. +/// +/// # Examples +/// +/// ```rust,ignore +/// walk_node(&node, &mut arm, &config, None); +/// assert!(arm.reads.contains(&"value_".to_string())); +/// ``` fn walk_node(node: &Entity, arm: &mut BodyArm, cfg: &BodyArmConfig, guard: Option<&str>) { match node.get_kind() { EntityKind::ThrowExpr => { @@ -1205,9 +1332,14 @@ fn walk_node(node: &Entity, arm: &mut BodyArm, cfg: &BodyArmConfig, guard: Optio } } -/// A call: either an overloaded-operator assignment (a write), a configured -/// lifecycle mutator (a `calls` fact), or an ordinary call walked for its -/// arguments. +/// Records facts for a call expression, including assignment writes, configured +/// lifecycle-mutator calls, and facts discovered while walking its arguments. +/// +/// # Examples +/// +/// ```ignore +/// call_expr(&node, &mut arm, &config, None); +/// ``` #[cfg(feature = "libclang")] fn call_expr(node: &Entity, arm: &mut BodyArm, cfg: &BodyArmConfig, guard: Option<&str>) { let name = node.get_name().unwrap_or_default(); @@ -1254,8 +1386,17 @@ fn call_expr(node: &Entity, arm: &mut BodyArm, cfg: &BodyArmConfig, guard: Optio } } -/// An `if`: walk the condition for its reads, then each branch — threading the -/// J1 guard into whichever branch the condition proves the member ABSENT in. +/// Walks an `if` statement, recording condition reads and propagating absence guards into the applicable branch. +/// +/// An enclosing guard is preserved when the condition does not establish a new absence guard. +/// +/// # Examples +/// +/// ```text +/// if (!value.member) { +/// value.member = replacement; +/// } +/// ``` #[cfg(feature = "libclang")] fn if_stmt(node: &Entity, arm: &mut BodyArm, cfg: &BodyArmConfig, guard: Option<&str>) { let children = node.get_children(); @@ -1281,9 +1422,14 @@ fn if_stmt(node: &Entity, arm: &mut BodyArm, cfg: &BodyArmConfig, guard: Option< } } -/// Record an assignment to `lhs` as a write, and as a J1 guarded write when -/// the enclosing branch is absence-guarded on that same member. -#[cfg(feature = "libclang")] +/// Records the assignment target as a member write and, when applicable, as an absence-guarded write. +/// +/// # Examples +/// +/// ```ignore +/// record_write(&mut arm, &lhs, Some("value")); +/// assert!(arm.writes.contains(&"value".to_string())); +/// ``` fn record_write(arm: &mut BodyArm, lhs: &Entity, guard: Option<&str>) { if let Some(member) = assignment_target(lhs) { if guard == Some(member.as_str()) { @@ -1293,10 +1439,18 @@ fn record_write(arm: &mut BodyArm, lhs: &Entity, guard: Option<&str>) { } } -/// The own data member an assignment's left-hand side ultimately names, seeing -/// through the wrappers libclang inserts plus subscripting and dereference -/// (`arr_[i] = v` and `*ptr_ = v` both write the member). `None` when the -/// target is anything else — a local, a parameter, or another object's member. +/// Identifies the own data member targeted by an assignment, including members +/// accessed through parentheses, unexposed expressions, or array subscripting. +/// +/// Returns `None` for locals, parameters, members of other objects, and +/// unsupported expression kinds. +/// +/// # Examples +/// +/// ```ignore +/// let member = assignment_target(&lhs); +/// assert_eq!(member.as_deref(), Some("value_")); +/// ``` #[cfg(feature = "libclang")] pub(crate) fn assignment_target(lhs: &Entity) -> Option { let mut cur = *lhs; @@ -1334,11 +1488,21 @@ pub(crate) fn own_member_name(e: &Entity) -> Option { } } -/// The operator spelling of a binary operator, read off the token stream. +/// Extracts the spelling of the operator from a binary-operator expression. /// -/// libclang exposes no binary-operator kind, so the operator is the first -/// punctuation token that starts at or after the end of the left operand. -#[cfg(feature = "libclang")] +/// The operator is identified as the first punctuation token at or after the +/// end of the left operand. +/// +/// # Returns +/// +/// The operator spelling, or `None` when the expression lacks the required +/// source range or left operand. +/// +/// # Examples +/// +/// ```rust,ignore +/// assert_eq!(binary_operator_spelling(&node).as_deref(), Some("+")); +/// ``` pub(crate) fn binary_operator_spelling(node: &Entity) -> Option { let lhs_end = node .get_children() @@ -1357,7 +1521,26 @@ pub(crate) fn binary_operator_spelling(node: &Entity) -> Option { .map(|t| t.get_spelling()) } -/// Is this unary operator an increment or decrement (prefix or postfix)? +/// Determines whether an AST entity represents a prefix or postfix increment or decrement operator. + +/// + +/// # Examples + +/// + +/// ```ignore + +/// let is_increment_or_decrement = unary_operator_is_inc_dec(&entity); + +/// ``` + +/// + +/// Returns `false` when the entity has no source range or its first and last tokens are + +/// neither `++` nor `--`. + #[cfg(feature = "libclang")] pub(crate) fn unary_operator_is_inc_dec(node: &Entity) -> bool { let Some(range) = node.get_range() else { @@ -1371,15 +1554,17 @@ pub(crate) fn unary_operator_is_inc_dec(node: &Entity) -> bool { is_inc_dec(tokens.first()) || is_inc_dec(tokens.last()) } -/// The J1 fact's condition half: does this `if` condition test ONE own member -/// for absence or presence, and which branch does that make the guarded one? +/// Classifies a simple condition that guards an own-member write. /// -/// Deliberately conservative — a compound condition (`&&` / `||`), a condition -/// naming more than one own member, or a shape not in the table below yields -/// `None`, so the write is recorded as a plain write. That is the safe -/// direction: a missed guard classifies the method as `Compute`/`Normalize`, -/// never as a false schema default. -#[cfg(feature = "libclang")] +/// Compound conditions, conditions involving multiple own members, and unsupported +/// condition shapes return `None`. +/// +/// # Examples +/// +/// ```ignore +/// let result = absence_guard(&condition); +/// assert_eq!(result, Some(("value_".to_owned(), GuardedBranch::Else))); +/// ``` fn absence_guard(cond: &Entity) -> Option<(String, GuardedBranch)> { let mut members = Vec::new(); collect_own_members(cond, &mut members); @@ -1420,23 +1605,28 @@ fn absence_guard(cond: &Entity) -> Option<(String, GuardedBranch)> { Some((member.clone(), branch)) } -/// Is the condition's own-member reference the SUBJECT of the test, or does the -/// condition merely MENTION it? -/// -/// `if (ptr_)`, `if (!ptr_)`, `if (ptr_ == nullptr)` and `if (name_.empty())` -/// all test the member itself. `if (is_ready(ptr_))` and `if (count_ + 1)` do -/// not: there the member is an argument or an operand, and the predicate's -/// result says nothing about whether the member is absent. Before this check -/// both were classified as guards, because the branch table reads TOKENS -/// across the whole condition and never asked where in it the member sits — so -/// `ptr_ = make()` in the other branch was recorded as a schema default. -/// -/// The walk descends only through positions that keep the member the subject: -/// wrappers and parentheses, a logical `!`, either side of a comparison, and -/// the RECEIVER of a call. Never a call's arguments, never an operand of -/// arithmetic, and never a dereference — `if (*ptr_)` tests the pointee, and -/// the member holds the same address whichever way that goes. -#[cfg(feature = "libclang")] +/// Determines whether a condition tests an own-member reference as its subject. +/// +/// Member references are considered subjects when they appear directly, through +/// wrappers or parentheses, under logical negation or comparison, or as the +/// receiver of a qualifying call. References used as call arguments, arithmetic +/// operands, dereferenced values, or unrelated operators are not subjects. +/// +/// # Arguments +/// +/// * `node` - The condition expression to inspect. +/// * `depth` - Remaining traversal depth. +/// +/// # Returns +/// +/// `true` if the expression tests an own-member reference as its subject, +/// `false` otherwise. +/// +/// # Examples +/// +/// ```ignore +/// assert!(member_is_condition_subject(condition, 32)); +/// ``` fn member_is_condition_subject(node: &Entity, depth: u32) -> bool { // Bounded so a cyclic cursor graph cannot loop; expression nesting in a // condition is far shallower than this. @@ -1487,10 +1677,14 @@ fn member_is_condition_subject(node: &Entity, depth: u32) -> bool { } } -/// The operator spelling of a PREFIX unary operator, read off the token stream -/// — libclang exposes no unary-operator kind either. Postfix `i++` yields `i`, -/// which is what callers testing for `!` want. -#[cfg(feature = "libclang")] +/// Returns the spelling of the first token in a cursor's source range. +/// +/// # Examples +/// +/// ```ignore +/// let spelling = unary_operator_spelling(&node); +/// assert_eq!(spelling.as_deref(), Some("!")); +/// ``` fn unary_operator_spelling(node: &Entity) -> Option { Some(node.get_range()?.tokenize().first()?.get_spelling()) } @@ -1506,11 +1700,29 @@ fn collect_own_members(node: &Entity, out: &mut Vec) { } } -/// The thrown exception's type name. `throw X(...)` nests the operand under -/// wrapper cursors, so recurse for the first node yielding a concrete, -/// non-void type name. -#[cfg(feature = "libclang")] +/// Extracts the first concrete, non-`void` type name from a throw expression. +/// +/// # Examples +/// +/// ```ignore +/// let exception_type = thrown_type_name(&throw_cursor); +/// assert_eq!(exception_type.as_deref(), Some("std::runtime_error")); +/// ``` +/// +/// Returns `None` when the expression does not contain a concrete exception type. pub(crate) fn thrown_type_name(throw: &Entity) -> Option { + /// Finds the first non-`void` type name in an entity and its descendants. + /// + /// Traversal follows child order and returns the first type whose bare name is non-empty. + /// + /// # Examples + /// + /// ```ignore + /// let type_name = first_typed(&entity); + /// assert_eq!(type_name.as_deref(), Some("Widget")); + /// ``` + /// + /// Returns `None` when no descendant has a usable non-`void` type name. fn first_typed(e: &Entity) -> Option { if let Some(t) = e.get_type() { let name = bare_type_name(&t.get_display_name()); @@ -1523,8 +1735,18 @@ pub(crate) fn thrown_type_name(throw: &Entity) -> Option { throw.get_children().iter().find_map(first_typed) } -/// The receiver of a method call (`repo_` in `repo_.Save()`), or `None` for an -/// implicit-`this` call — the callee reference's own base. +/// Identifies the receiver of a method call, such as `repo_` in `repo_.save()`. +/// +/// Returns `None` for calls using the implicit `this` receiver or when the receiver +/// cannot be resolved. +/// +/// # Examples +/// +/// ```rust,no_run +/// # use clang::Entity; +/// # let call: Entity = todo!(); +/// assert_eq!(call_receiver(&call), Some("repo_".to_owned())); +/// ``` #[cfg(feature = "libclang")] pub(crate) fn call_receiver(call: &Entity) -> Option { let name = call.get_name()?; @@ -1544,7 +1766,26 @@ pub(crate) fn call_receiver(call: &Entity) -> Option { None } -/// `List` / `foo::Bar` → a stable bare type name for the `raises` object. +/// Extracts the unqualified base name from a displayed C++ type. + +/// + +/// Removes `class` or `struct` prefixes, template arguments, and namespace + +/// qualifiers. + +/// + +/// # Examples + +/// + +/// ``` + +/// assert_eq!(bare_type_name("class foo::Bar"), "Bar"); + +/// ``` + #[cfg(feature = "libclang")] pub(crate) fn bare_type_name(display: &str) -> String { let s = display @@ -1564,17 +1805,14 @@ pub(crate) fn bare_type_name(display: &str) -> String { #[cfg(all(test, feature = "libclang"))] pub(crate) static CLANG_TEST_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(()); -/// A per-process, per-call suffix for a test fixture directory. -/// -/// [`CLANG_TEST_LOCK`] serialises libclang WITHIN a process, and that is all it -/// can do: this repository runs tests under nextest, which gives every `#[test]` -/// its OWN process, so a `Mutex` in one of them is invisible to the others. -/// Twelve tests once shared `cpp_arm_shapes/f.cpp`, and `File::create` -/// truncates, so one process could empty the file while another was inside -/// `parse()`. That parse then found no methods and the caller's index panicked -/// with `no entry found for key` — intermittently, which is the worst way to -/// find out. Unique paths remove the sharing instead of trying to coordinate -/// it. +/// Generates a suffix that is unique for each call within the current process. +/// +/// # Examples +/// +/// ``` +/// let suffix = fixture_salt(); +/// assert!(!suffix.is_empty()); +/// ``` #[cfg(test)] pub(crate) fn fixture_salt() -> String { use std::sync::atomic::{AtomicU32, Ordering}; @@ -1591,7 +1829,19 @@ mod arm_tests { use super::*; use std::io::Write; - /// Every method under `e`, by name. + /// Collects methods, constructors, and destructors beneath an entity, keyed by name. + /// + /// When both a declaration and an out-of-line definition share a name, the definition + /// is retained. + /// + /// # Examples + /// + /// ```ignore + /// let mut methods = std::collections::BTreeMap::new(); + /// methods(&entity, &mut methods); + /// assert!(methods.contains_key("run")); + /// ``` + fn methods<'a>(e: &Entity<'a>, out: &mut BTreeMap>) { fn methods<'a>(e: &Entity<'a>, out: &mut BTreeMap>) { for c in e.get_children() { if matches!( @@ -2244,8 +2494,25 @@ mod walker_tests { use super::*; use std::io::Write; - /// Write `src` to a fresh temp file under a name-scoped dir (mirrors - /// `arm_tests::arm_of`'s fixture-writing pattern), returning its path. + /// Writes C++ source to a uniquely named temporary fixture file. + /// + /// # Examples + /// + /// ``` + /// let path = write_fixture("example", "int main() {}"); + /// assert_eq!(std::fs::read_to_string(&path).unwrap(), "int main() {}"); + /// std::fs::remove_dir_all(path.parent().unwrap()).unwrap(); + /// ``` + /// + /// # Panics + /// + /// Panics if the temporary directory or fixture file cannot be created, or if + /// the source cannot be written. + /// + /// # Parameters + /// + /// * `name` - Name used to identify the temporary fixture directory. + /// * `src` - C++ source to write. fn write_fixture(name: &str, src: &str) -> std::path::PathBuf { let dir = std::env::temp_dir().join(format!("cpp_walker_{name}_{}", fixture_salt())); std::fs::create_dir_all(&dir).unwrap(); diff --git a/crates/ruff_cpp_spo/src/events.rs b/crates/ruff_cpp_spo/src/events.rs index 0c38982ab20629..027b700c8a8f22 100644 --- a/crates/ruff_cpp_spo/src/events.rs +++ b/crates/ruff_cpp_spo/src/events.rs @@ -116,6 +116,14 @@ impl EventKind { } impl fmt::Display for EventKind { + /// Formats the event kind using its stable string representation. + /// + /// # Examples + /// + /// ``` + /// let kind = EventKind::FunctionEnter; + /// assert_eq!(kind.to_string(), kind.as_str()); + /// ``` fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str(self.as_str()) } @@ -145,6 +153,15 @@ pub enum ScopeKind { } impl ScopeKind { + /// Provides the stable name of this scope kind. + /// + /// # Examples + /// + /// ``` + /// assert_eq!(ScopeKind::Loop.as_str(), "Loop"); + /// ``` + /// + /// Each scope kind maps to a fixed string representation. pub fn as_str(self) -> &'static str { match self { Self::Function => "Function", @@ -165,8 +182,16 @@ impl ScopeKind { } } - /// Does a loop's exit close a back edge? The one control relation that is - /// structurally certain without a CFG. + /// Determines whether this scope kind represents a loop. + /// + /// # Examples + /// + /// ``` + /// assert!(ScopeKind::For.is_loop()); + /// assert!(!ScopeKind::Block.is_loop()); + /// ``` + /// + /// Returns `true` for `for`, `while`, `do`, and range-based `for` loops; `false` otherwise. fn is_loop(self) -> bool { matches!(self, Self::For | Self::While | Self::Do | Self::RangeFor) } @@ -186,6 +211,13 @@ pub enum SymKind { } impl SymKind { + /// Provides the stable string representation of this symbol kind. + /// + /// # Examples + /// + /// ``` + /// assert_eq!(SymKind::Member.as_str(), "member"); + /// ``` pub fn as_str(self) -> &'static str { match self { Self::Member => "member", @@ -215,6 +247,15 @@ pub enum Role { } impl Role { + /// Returns the stable string representation of a role. + /// + /// # Examples + /// + /// ``` + /// use crate::events::Role; + /// + /// assert_eq!(Role::Param.as_str(), "param"); + /// ``` pub fn as_str(self) -> &'static str { match self { Self::Own => "own", @@ -239,6 +280,14 @@ pub enum Prov { } impl Prov { + /// Provides the stable lowercase label for this provenance value. + /// + /// # Examples + /// + /// ``` + /// assert_eq!(Prov::Clang.as_str(), "clang"); + /// assert_eq!(Prov::Walk.as_str(), "walk"); + /// ``` pub fn as_str(self) -> &'static str { match self { Self::Clang => "clang", @@ -320,6 +369,19 @@ pub struct Symbols { } impl Symbols { + /// Interns a symbol and returns its stable identifier. + /// + /// Symbols with the same kind, role, and name share an identifier; the first + /// provenance value is retained for each unique symbol. + /// + /// # Examples + /// + /// ``` + /// let mut symbols = Symbols::default(); + /// let id = symbols.intern(SymKind::Member, Role::OwnMember, "value", Prov::Walk); + /// + /// assert_eq!(id, "s0"); + /// ``` pub fn intern(&mut self, kind: SymKind, role: Role, name: &str, prov: Prov) -> String { let key = (kind, role, name.to_string()); if let Some(id) = self.index.get(&key) { @@ -331,6 +393,14 @@ impl Symbols { format!("s{id}") } + /// Iterates over interned symbols with their stable identifiers and metadata. + /// + /// # Examples + /// + /// ``` + /// let symbols = Symbols::default(); + /// assert_eq!(symbols.rows().count(), 0); + /// ``` pub fn rows(&self) -> impl Iterator { self.rows .iter() @@ -338,10 +408,28 @@ impl Symbols { .map(|(i, (k, r, n, p))| (format!("s{i}"), *k, *r, n.as_str(), *p)) } + /// Reports the number of interned symbols. + /// + /// # Examples + /// + /// ``` + /// let symbols = Symbols::default(); + /// assert_eq!(symbols.len(), 0); + /// ``` + /// + /// Returns the number of symbols stored in the table. pub fn len(&self) -> usize { self.rows.len() } + /// Determines whether the symbol table contains no symbols. + /// + /// # Examples + /// + /// ``` + /// let symbols = Symbols::default(); + /// assert!(symbols.is_empty()); + /// ``` pub fn is_empty(&self) -> bool { self.rows.is_empty() } @@ -366,6 +454,14 @@ struct Walk<'s> { } impl<'s> Walk<'s> { + /// Creates a walker backed by the provided shared symbol table. + /// + /// # Examples + /// + /// ``` + /// let mut symbols = Symbols::default(); + /// let _walker = Walk::new(&mut symbols); + /// ``` fn new(syms: &'s mut Symbols) -> Self { Self { syms, @@ -375,21 +471,66 @@ impl<'s> Walk<'s> { } } + /// Gets the ID of the currently active scope, or `0` when no scope is active. + /// + /// # Examples + /// + /// ```ignore + /// let scope_id = walk.cur(); + /// ``` fn cur(&self) -> u32 { self.stack.last().copied().unwrap_or(0) } + /// Gets the identifier of the enclosing scope. + /// + /// # Examples + /// + /// ``` + /// # struct Walk { stack: Vec } + /// # impl Walk { + /// # fn parent(&self) -> Option { + /// # let n = self.stack.len(); + /// # (n >= 2).then(|| self.stack[n - 2]) + /// # } + /// # } + /// let walk = Walk { stack: vec![10, 20] }; + /// assert_eq!(walk.parent(), Some(10)); + /// ``` fn parent(&self) -> Option { let n = self.stack.len(); (n >= 2).then(|| self.stack[n - 2]) } + /// Returns the source-file byte offset where an entity begins, or `0` when no source range is available. + /// + /// # Examples + /// + /// ``` + /// # use clang::Entity; + /// # fn example(entity: &Entity<'_>) { + /// let offset = anchor(entity); + /// assert!(offset >= 0); + /// # } + /// ``` fn anchor(e: &Entity<'_>) -> u32 { e.get_range() .map(|r| r.get_start().get_file_location().offset) .unwrap_or(0) } + /// Appends a walker-originated event for an entity in the current scope. + /// + /// The event receives a sequence number, source anchor, current scope, and + /// parent scope, while its subject, object, control metadata, and type + /// relation remain unset. + /// + /// # Examples + /// + /// ```ignore + /// let index = walk.push(EventKind::Read, &entity); + /// assert_eq!(walk.events[index].kind, EventKind::Read); + /// ``` fn push(&mut self, kind: EventKind, e: &Entity<'_>) -> usize { let seq = u32::try_from(self.events.len()).unwrap_or(u32::MAX); self.events.push(OreEvent { @@ -407,8 +548,18 @@ impl<'s> Walk<'s> { self.events.len() - 1 } - /// Open a scope, run `body`, close it. Enter/exit carry the scope's own id - /// and its kind symbol, so a nested sequence cannot flatten into a sibling. + /// Records a nested scope and its matching entry and exit events. + /// + /// The scope is linked to the currently active parent, and loop scopes mark + /// their exit event with a `back_edge` control label. + /// + /// # Examples + /// + /// ```rust,ignore + /// walker.scoped(ScopeKind::Loop, entity, |walker| { + /// walker.children(entity); + /// }); + /// ``` fn scoped(&mut self, kind: ScopeKind, e: &Entity<'_>, body: F) { let id = u32::try_from(self.scopes.len()).unwrap_or(u32::MAX); let depth = u16::try_from(self.stack.len()).unwrap_or(u16::MAX); @@ -441,16 +592,33 @@ impl<'s> Walk<'s> { self.stack.pop(); } + /// Visits each direct child entity with the walker. + /// + /// # Examples + /// + /// ```ignore + /// for child in entity.get_children() { + /// walker.node(&child); + /// } + /// ``` fn children(&mut self, e: &Entity<'_>) { for c in e.get_children() { self.node(&c); } } - /// A construct's body. A `CompoundStmt` here is the braces of the - /// construct we already opened a scope for, so it is walked THROUGH — - /// otherwise `if (x) f();` and `if (x) { f(); }` would differ by a scope, - /// and a tokenizer would learn brace style instead of behavior. + /// Walks a construct body without introducing a redundant block scope. + /// + /// Braced and unbraced bodies are handled uniformly, preserving equivalent + /// scope structures for constructs such as `if (x) f();` and + /// `if (x) { f(); }`. + /// + /// # Examples + /// + /// ```text + /// if (condition) statement; + /// if (condition) { statement; } + /// ``` fn body(&mut self, e: &Entity<'_>) { if e.get_kind() == EntityKind::CompoundStmt { self.children(e); @@ -459,7 +627,24 @@ impl<'s> Walk<'s> { } } - /// The role of a referenced declaration. `prov=clang` — libclang resolved it. + /// Classifies a referenced declaration by role, symbol kind, and provenance. + /// + /// # Examples + /// + /// ```ignore + /// let (role, kind, provenance) = ref_role(&entity); + /// assert_eq!(role, Role::Param); + /// assert_eq!(kind, SymKind::Param); + /// assert_eq!(provenance, Prov::Clang); + /// ``` + /// + /// # Returns + /// + /// A tuple containing the declaration's role, symbol kind, and resolution provenance. Unresolved references use `Role::Unknown`, `SymKind::Other`, and `Prov::Walk`. + /// + /// # Parameters + /// + /// * `e` - The entity whose referenced declaration is classified. fn ref_role(e: &Entity<'_>) -> (Role, SymKind, Prov) { match e.get_reference().map(|r| r.get_kind()) { Some(EntityKind::ParmDecl) => (Role::Param, SymKind::Param, Prov::Clang), @@ -469,6 +654,15 @@ impl<'s> Walk<'s> { } } + /// Visits a Clang entity and records its behavioral events and structural scopes. + /// + /// # Examples + /// + /// ```ignore + /// let before = walk.events.len(); + /// walk.node(&entity); + /// assert!(walk.events.len() >= before); + /// ``` fn node(&mut self, e: &Entity<'_>) { match e.get_kind() { EntityKind::CompoundStmt => self.scoped(ScopeKind::Block, e, |w| w.children(e)), @@ -787,7 +981,17 @@ impl<'s> Walk<'s> { } } - /// Record a write/read-modify-write against whatever the LHS names. + /// Records a write or read-modify-write event for the symbol named by the left-hand side. + /// + /// Member assignment targets are recorded as owned members; other targets use their + /// inferred symbol kind, role, and provenance. + /// + /// # Examples + /// + /// ```ignore + /// walk.write_event(&lhs, EventKind::Write); + /// ``` + fn write_event(&mut self, lhs: &Entity<'_>, kind: EventKind) { fn write_event(&mut self, lhs: &Entity<'_>, kind: EventKind) { let (sym, prov) = if let Some(m) = assignment_target(lhs) { ( @@ -804,13 +1008,24 @@ impl<'s> Walk<'s> { self.events[i].prov = prov; } - /// Walk an assignment target's children WITHOUT re-recording the target - /// itself as a read — mirroring the shipped arm, which walks the LHS with - /// `walk_body` precisely to skip its own top-level `MemberRefExpr`. + /// Walks an assignment target's children without recording the target itself as a read. + /// + /// # Examples + /// + /// ```ignore + /// walker.node_skipping_top_member(lhs); + /// ``` fn node_skipping_top_member(&mut self, lhs: &Entity<'_>) { self.children(lhs); } + /// Records a function or method call and traverses its receiver and arguments. + /// + /// # Examples + /// + /// ```ignore + /// walker.call(&call_entity); + /// ``` fn call(&mut self, e: &Entity<'_>) { let name = e.get_name().unwrap_or_default(); let referenced = e.get_reference(); @@ -869,8 +1084,16 @@ impl<'s> Walk<'s> { // Entry points // ───────────────────────────────────────────────────────────────────────── -/// The method IRI, byte-identical to what `ruff_spo_triplet::expand` builds -/// for the same method — the join key every consumer relies on. +/// Builds the canonical qualified identifier for a C++ method, including parameter types, constness, and reference qualification. +/// +/// Returns `None` when the method has no name or semantic parent. +/// +/// # Examples +/// +/// ``` +/// let iri = "Namespace::Type.method(int, std::string) const &"; +/// assert_eq!(iri, "Namespace::Type.method(int, std::string) const &"); +/// ``` fn method_iri(m: &Entity<'_>) -> Option { let name = m.get_name()?; let parent = m.get_semantic_parent()?; @@ -897,6 +1120,17 @@ fn method_iri(m: &Entity<'_>) -> Option { )) } +/// Identifies whether a Clang entity kind represents a callable declaration. +/// +/// # Examples +/// +/// ``` +/// assert!(is_callable(EntityKind::Method)); +/// assert!(!is_callable(EntityKind::Namespace)); +/// ``` +/// +/// Returns `true` for methods, constructors, destructors, conversion functions, +/// and function declarations; `false` for other entity kinds. fn is_callable(k: EntityKind) -> bool { matches!( k, @@ -908,6 +1142,19 @@ fn is_callable(k: EntityKind) -> bool { ) } +/// Collects callable method definitions from a Clang entity tree that belong to the translation unit's main file. +/// +/// Matching methods are converted to ordered method data and appended to `out`; declarations from +/// included headers are skipped. +/// +/// # Examples +/// +/// ```ignore +/// collect(&root, tu, &mut symbols, &mut methods, main_file, &arm_cfg); +/// ``` +/// +/// `main` identifies the translation unit's source file. `tu` and `arm_cfg` provide context for +/// method extraction, while `syms` stores shared symbol identities. fn collect( e: &Entity<'_>, tu: &str, @@ -933,6 +1180,19 @@ fn collect( } } +/// Builds the ordered behavioral representation and metadata for a C++ method. +/// +/// The result combines the method's ordered events and nested scopes with its +/// identity, qualifiers, access, override information, shipped set counts, and +/// centroid classification. +/// +/// # Examples +/// +/// ```ignore +/// let ore = method_ore(&method, translation_unit, iri, &mut symbols, &arm_cfg); +/// assert_eq!(ore.iri, iri); +/// ``` +fn method_ore fn method_ore( m: &Entity<'_>, tu: &str, @@ -1026,14 +1286,25 @@ fn method_ore( } } -/// Walk ONE translation unit and return every method definition's ordered ore. +/// Walks a translation unit and collects ordered ore for its method definitions. +/// +/// Parse diagnostics at error severity are included in the returned translation-unit data. /// /// # Errors /// -/// [`crate::WalkError::Libclang`] if libclang fails to initialise; -/// [`crate::WalkError::Parse`] if the TU fails to parse. Parse DIAGNOSTICS are -/// returned rather than swallowed — a partial AST must be visible, for the -/// same reason `walk_tu_with_diagnostics` exists. +/// Returns [`crate::WalkError::Libclang`] if libclang cannot be initialized, or +/// [`crate::WalkError::Parse`] if parsing fails. +/// +/// # Examples +/// +/// ```no_run +/// use std::path::Path; +/// +/// let mut symbols = Symbols::default(); +/// let ore = walk_tu_events(Path::new("example.cpp"), &[], &mut symbols)?; +/// # let _: TuOre = ore; +/// # Ok::<(), crate::WalkError>(()) +/// ``` pub fn walk_tu_events( path: &Path, args: &[String], @@ -1116,6 +1387,14 @@ mod tests { m.events.iter().map(|e| e.kind.as_str()).collect() } + /// Collects the stable names of a method's scopes in traversal order. + /// + /// # Examples + /// + /// ``` + /// let method = MethodOre::default(); + /// assert!(scope_kinds(&method).is_empty()); + /// ``` fn scope_kinds(m: &MethodOre) -> Vec<&'static str> { m.scopes.iter().map(|s| s.kind.as_str()).collect() } diff --git a/crates/ruff_cpp_spo/src/lib.rs b/crates/ruff_cpp_spo/src/lib.rs index 62e205f1b2f76b..e7b1b0c0fe24cf 100644 --- a/crates/ruff_cpp_spo/src/lib.rs +++ b/crates/ruff_cpp_spo/src/lib.rs @@ -314,11 +314,25 @@ fn collect_cpp_files(dir: &Path, out: &mut Vec) { } } -/// The shared dedup loop behind [`extract_dir`] / [`extract_tree`]: walk each -/// file as its own TU, dedup classes by fully-qualified name into a -/// deterministic `BTreeMap`, skip per-TU [`WalkError::Parse`] failures, and -/// propagate a non-recoverable [`WalkError::Libclang`] (codex P2, PR #13). -#[cfg(feature = "libclang")] +/// Walks translation-unit files and builds a deterministic model graph. +/// +/// Parse failures for individual files are skipped. Libclang failures stop processing +/// and are returned. Classes with the same fully qualified name are merged, including +/// method body information from later translation units. +/// +/// # Examples +/// +/// ```no_run +/// let files = vec![std::path::PathBuf::from("example.cpp")]; +/// let args = vec!["-std=c++17".to_owned()]; +/// let graph = walk_files(&files, &args)?; +/// # let _: ruff_spo_triplet::ModelGraph = graph; +/// # Ok::<(), WalkError>(()) +/// ``` +/// +/// # Errors +/// +/// Returns [`WalkError::Libclang`] when libclang cannot process the files. fn walk_files(files: &[std::path::PathBuf], args: &[String]) -> Result { let mut seen: std::collections::BTreeMap = std::collections::BTreeMap::new(); for f in files { @@ -344,24 +358,18 @@ fn walk_files(files: &[std::path::PathBuf], args: &[String]) -> Result ModelGraph { let mut rec = Model::new("Tesseract::Recognizer"); rec.bases.push(CppBase { diff --git a/crates/ruff_spo_triplet/src/expand.rs b/crates/ruff_spo_triplet/src/expand.rs index d5141cd2c1c15d..e776341bb1ebce 100644 --- a/crates/ruff_spo_triplet/src/expand.rs +++ b/crates/ruff_spo_triplet/src/expand.rs @@ -762,6 +762,19 @@ impl Expander { ); } + /// Expands a C++ method into its function, signature, property, and body-action triples. + /// + /// Overloaded methods receive distinct identities based on their parameter types, + /// const qualification, and ref-qualification. + /// + /// # Examples + /// + /// ```ignore + /// expander.cpp_method("cpp", "cpp:Widget", &method); + /// ``` + /// + /// `ns` identifies the namespace used for overridden methods, `model_iri` identifies + /// the owning C++ model, and `method` supplies the method declaration and body facts. fn cpp_method(&mut self, ns: &str, model_iri: &str, method: &CppMethod) { // Per-overload identity: append `()` so // overloaded methods (`void f(int)` + `void f(double)`) get distinct @@ -2417,9 +2430,15 @@ mod tests { // ────────────────── C++ machine-plane tests ────────────────── - /// A `Tesseract::Recognizer`-shaped model exercising every C++ match - /// arm. The expand half of the `CppClass → Triple` round-trip the - /// `ruff_cpp_spo` locked-shape test mirrors. + /// Builds a C++ model graph containing representative class, method, template, and declaration metadata. + /// + /// # Examples + /// + /// ``` + /// let graph = cpp_fixture(); + /// assert_eq!(graph.namespace, "cpp"); + /// assert_eq!(graph.models.len(), 1); + /// ``` fn cpp_fixture() -> ModelGraph { let mut rec = Model::new("Tesseract::Recognizer"); rec.bases.push(CppBase { diff --git a/crates/ruff_spo_triplet/src/ir.rs b/crates/ruff_spo_triplet/src/ir.rs index 5e8182e94893d0..15de927afcb5b4 100644 --- a/crates/ruff_spo_triplet/src/ir.rs +++ b/crates/ruff_spo_triplet/src/ir.rs @@ -819,7 +819,16 @@ pub enum CppRefQualifier { } impl CppRefQualifier { - /// The C++ spelling, as it appears in a method IRI's suffix. + /// Provides the C++ reference qualifier spelling used in method IRI suffixes. + /// + /// # Examples + /// + /// ``` + /// assert_eq!(CppRefQualifier::LValue.spelling(), "&"); + /// assert_eq!(CppRefQualifier::RValue.spelling(), "&&"); + /// ``` + /// + /// Returns the C++ reference qualifier as `&` or `&&`. #[must_use] pub fn spelling(self) -> &'static str { match self { diff --git a/crates/ruff_spo_triplet/src/reassemble.rs b/crates/ruff_spo_triplet/src/reassemble.rs index 96d35ebf469a2f..2ccb3b76bf084a 100644 --- a/crates/ruff_spo_triplet/src/reassemble.rs +++ b/crates/ruff_spo_triplet/src/reassemble.rs @@ -85,15 +85,18 @@ struct MethodAcc { guarded_writes: Vec, } -/// Reassemble the C++ machine-plane projection of a triple set into a -/// [`ModelGraph`]. +/// Reassembles a canonical C++ model graph from expanded triples. /// -/// See the module docs for the scope, the round-trip property, and why -/// method identity is recovered from `has_param_type` rather than the IRI -/// suffix. The returned graph is canonicalised (collections sorted, the -/// three never-emitted fields blanked to their defaults) so it compares -/// equal to `expand`'s emitted projection of the source graph. -#[must_use] +/// Method identities and qualifiers are recovered from the emitted triples and +/// method IRIs. Unrelated or incomplete triples are ignored. +/// +/// # Examples +/// +/// ``` +/// let graph = reassemble(&[]); +/// assert!(graph.models.is_empty()); +/// ``` +pub fn reassemble(triples: &[Triple]) -> ModelGraph { pub fn reassemble(triples: &[Triple]) -> ModelGraph { // The namespace is the prefix of any class anchor's subject IRI. let namespace = triples @@ -382,19 +385,18 @@ pub fn cpp_projection(graph: &ModelGraph) -> ModelGraph { projected } -/// Sort and de-duplicate every C++ collection, and blank the three fields -/// [`crate::expand`] never emits, so a reassembled graph and a source graph's -/// emitted projection compare equal regardless of source declaration order. +/// Canonicalizes the C++ representation so equivalent graphs compare deterministically. +/// +/// This clears properties that [`crate::expand`] does not emit, then sorts and +/// de-duplicates fields, bases, methods, method body relations, templates, +/// friends, macro uses, static assertions, and models. +/// +/// # Examples /// -/// De-duplication mirrors `expand`'s `(s, p, o)` dedup: a source graph can -/// carry the same fact twice (e.g. a template-id instantiated in several method -/// signatures yields several identical `template_instantiates`, or a member -/// harvested twice), but `expand` collapses identical triples, so `reassemble` -/// recovers each fact once. The projection must therefore collapse exact -/// duplicates too — otherwise a benign duplicate would read as a round-trip -/// difference. Real collisions (two entries sharing a sort key but differing in -/// content — the genuine overload-collision residual) are NOT equal, so dedup -/// keeps them and they still surface. +/// ``` +/// let mut graph = ModelGraph::default(); +/// canonicalize_cpp(&mut graph); +/// ``` fn canonicalize_cpp(graph: &mut ModelGraph) { for model in &mut graph.models { for field in &mut model.member_fields { diff --git a/crates/ruff_spo_triplet/src/recipe.rs b/crates/ruff_spo_triplet/src/recipe.rs index 726570874bd4f3..ed8152f384df77 100644 --- a/crates/ruff_spo_triplet/src/recipe.rs +++ b/crates/ruff_spo_triplet/src/recipe.rs @@ -36,36 +36,176 @@ pub trait BodyFacts { } impl BodyFacts for Function { + /// Returns the names of fields written by the body. + /// + /// # Examples + /// + /// ``` + /// let writes = function.writes(); + /// assert!(writes.contains(&"field".to_string())); + /// ``` fn writes(&self) -> &[String] { &self.writes } + /// Provides the names of fields read by the body. + /// + /// # Examples + /// + /// ``` + /// let fields = body.reads(); + /// assert!(fields.iter().all(|field| !field.is_empty())); + /// ``` + /// + /// Returns the fields accessed by the body. fn reads(&self) -> &[String] { &self.reads } + /// Provides the fields raised by the body. + /// + /// # Examples + /// + /// ``` + /// struct Facts { + /// raises: Vec, + /// } + /// + /// impl BodyFacts for Facts { + /// fn writes(&self) -> &[String] { &[] } + /// fn reads(&self) -> &[String] { &[] } + /// fn raises(&self) -> &[String] { &self.raises } + /// fn calls(&self) -> &[String] { &[] } + /// fn guarded_writes(&self) -> &[String] { &[] } + /// } + /// + /// let facts = Facts { + /// raises: vec![String::from("error")], + /// }; + /// assert_eq!(facts.raises(), &[String::from("error")]); + /// ``` fn raises(&self) -> &[String] { &self.raises } + /// Returns the calls recorded for the body. + /// + /// # Examples + /// + /// ``` + /// let calls = body_facts.calls(); + /// assert!(calls.iter().all(|call| !call.is_empty())); + /// ``` fn calls(&self) -> &[String] { &self.calls } + /// Provides the fields whose writes are guarded. + /// + /// # Examples + /// + /// ``` + /// struct Facts { + /// guarded_writes: Vec, + /// } + /// + /// impl BodyFacts for Facts { + /// fn writes(&self) -> &[String] { &[] } + /// fn reads(&self) -> &[String] { &[] } + /// fn raises(&self) -> &[String] { &[] } + /// fn calls(&self) -> &[String] { &[] } + /// fn guarded_writes(&self) -> &[String] { &self.guarded_writes } + /// } + /// + /// let facts = Facts { + /// guarded_writes: vec![String::from("value")], + /// }; + /// assert_eq!(facts.guarded_writes(), &["value"]); + /// ``` fn guarded_writes(&self) -> &[String] { &self.guarded_writes } } impl BodyFacts for CppMethod { + /// Returns the names of fields written by the body. + /// + /// # Examples + /// + /// ``` + /// let writes = function.writes(); + /// assert!(writes.contains(&"field".to_string())); + /// ``` fn writes(&self) -> &[String] { &self.writes } + /// Provides the names of fields read by the body. + /// + /// # Examples + /// + /// ``` + /// let fields = body.reads(); + /// assert!(fields.iter().all(|field| !field.is_empty())); + /// ``` + /// + /// Returns the fields accessed by the body. fn reads(&self) -> &[String] { &self.reads } + /// Provides the fields raised by the body. + /// + /// # Examples + /// + /// ``` + /// struct Facts { + /// raises: Vec, + /// } + /// + /// impl BodyFacts for Facts { + /// fn writes(&self) -> &[String] { &[] } + /// fn reads(&self) -> &[String] { &[] } + /// fn raises(&self) -> &[String] { &self.raises } + /// fn calls(&self) -> &[String] { &[] } + /// fn guarded_writes(&self) -> &[String] { &[] } + /// } + /// + /// let facts = Facts { + /// raises: vec![String::from("error")], + /// }; + /// assert_eq!(facts.raises(), &[String::from("error")]); + /// ``` fn raises(&self) -> &[String] { &self.raises } + /// Returns the calls recorded for the body. + /// + /// # Examples + /// + /// ``` + /// let calls = body_facts.calls(); + /// assert!(calls.iter().all(|call| !call.is_empty())); + /// ``` fn calls(&self) -> &[String] { &self.calls } + /// Provides the fields whose writes are guarded. + /// + /// # Examples + /// + /// ``` + /// struct Facts { + /// guarded_writes: Vec, + /// } + /// + /// impl BodyFacts for Facts { + /// fn writes(&self) -> &[String] { &[] } + /// fn reads(&self) -> &[String] { &[] } + /// fn raises(&self) -> &[String] { &[] } + /// fn calls(&self) -> &[String] { &[] } + /// fn guarded_writes(&self) -> &[String] { &self.guarded_writes } + /// } + /// + /// let facts = Facts { + /// guarded_writes: vec![String::from("value")], + /// }; + /// assert_eq!(facts.guarded_writes(), &["value"]); + /// ``` fn guarded_writes(&self) -> &[String] { &self.guarded_writes } @@ -107,13 +247,20 @@ pub enum RecipeCentroid { Empty, } -/// Classify a function's fact-set into its nearest recipe centroid. +/// Classifies a body’s facts into the first matching recipe centroid. +/// +/// Classification prioritizes compensating, cascading, guarding, and write-then-raise +/// behavior before distinguishing default, compute, normalize, observe, and empty bodies. +/// Guarded writes take precedence over compute and normalize classifications. +/// +/// # Examples /// -/// First-match-wins, in exactly the order of -/// `.claude/knowledge/fuzzy-recipe-codebook.md` §3. The J1 guard-write fact -/// (`guarded_writes`) splits [`RecipeCentroid::Default`] out **before** -/// [`RecipeCentroid::Compute`] / [`RecipeCentroid::Normalize`] are checked, -/// per the J1 finding recorded there. +/// ``` +/// # use ruff_spo_triplet::recipe::{classify, RecipeCentroid}; +/// # let facts = todo!(); +/// let centroid = classify(&facts); +/// assert_eq!(centroid, RecipeCentroid::Empty); +/// ``` #[must_use] pub fn classify(f: &F) -> RecipeCentroid { let writes = !f.writes().is_empty();