abcop 0.18.2

Must-have ABC complexity gate for AI development. Ruby, Rust, Python, Go, JS/TS, C/C++, PHP, Java, C#, Swift, Zig, Dart, Solidity, Haskell, ObjC
//! UsedOnce / NeverUsed contract vectors for the plain-C-family scope
//! collectors (C, C++, Objective-C).

use crate::paths::{Lang, parse_file_lang};

fn used(lang: Lang, src: &'static str) -> Vec<String> {
    let mut v: Vec<_> = super::used_once_offenses(
        &super::collect_scopes(
            src.as_bytes(),
            &parse_file_lang(src.as_bytes(), lang).expect("fixture parses"),
            lang,
        ),
        lang,
    )
        .into_iter()
        .map(|o| o.name)
        .collect();
    v.sort();
    v
}

fn dead(lang: Lang, src: &'static str) -> Vec<String> {
    let mut v: Vec<_> = super::never_used_offenses(
        &super::collect_scopes(
            src.as_bytes(),
            &parse_file_lang(src.as_bytes(), lang).expect("fixture parses"),
            lang,
        ),
        lang,
    )
        .into_iter()
        .map(|o| o.name)
        .collect();
    v.sort();
    v
}

// ---- UsedOnce / NeverUsed over C / C++ / Objective-C ----
// One vector per dispatcher shape: dead binding, pure inline candidate,
// compound/`++` rejection, member-slot distinctness, loop-head protocol.

#[test]
fn c_flags_dead_binding_and_pure_inline_candidate() {
    let src = "int main(void) {\n  int dead = 1;\n  int cand = 2 + 3;\n  return cand + 0;\n}";
    assert_eq!(dead(Lang::C, src), vec!["dead".to_string()]);
    assert_eq!(used(Lang::C, src), vec!["cand".to_string()]);
}

#[test]
fn c_pointer_wrappers_still_bind_the_name() {
    // `char *p = s;`: pointer_declarator wraps the identifier; bare alias.
    let src = "void f(char *s) {\n  char *p = s;\n  use(p);\n}";
    assert_eq!(used(Lang::C, src), vec!["p"]);
}

#[test]
fn c_immediate_call_chain_yes_intervening_and_loop_no() {
    assert_eq!(
        used(Lang::C, "int f(void) {\n  int a = helper();\n  return a;\n}"),
        vec!["a"]
    );
    assert_eq!(
        used(
            Lang::C,
            "int f(void) {\n  int a = helper();\n  side();\n  return a;\n}"
        ),
        Vec::<String>::new()
    );
    assert_eq!(
        used(
            Lang::C,
            "int f(int n) {\n  int a = helper();\n  for (int i = 0; i < n; i++) { use(a); }\n  return 0;\n}"
        ),
        Vec::<String>::new()
    );
}

#[test]
fn c_dead_call_keeps_initializer() {
    let src = b"int f(void) {\n  int gone = helper();\n  return 1;\n}";
    let f = super::never_used_offenses(
        &super::collect_scopes(src, &parse_file_lang(src, Lang::C).unwrap(), Lang::C),
        Lang::C,
    );
    assert_eq!(f.len(), 1);
    assert_eq!(f[0].name, "gone");
    assert!(f[0].keep_init);
}

#[test]
fn c_compound_ops_and_increments_are_not_inline_candidates() {
    let src = "int main(void) {\n  int t = 0;\n  t += 1;\n  t++;\n  return t;\n}";
    assert_eq!(used(Lang::C, src), Vec::<String>::new());
}

#[test]
fn c_loop_head_vars_are_protocol() {
    // `i` is written once and read once inside the for -- the loop head
    // must be excluded so no inlining suggestion fires on it.
    let src = "int main(void) {\n  int total = 0;\n  for (int i = 0; i < 3; i++) { total += i; }\n  return total;\n}";
    assert_eq!(used(Lang::C, src), Vec::<String>::new());
    assert_eq!(dead(Lang::C, src), Vec::<String>::new());
}

#[test]
fn c_globals_are_outside_local_analysis_and_object_writes_read_the_base() {
    // file-scope statics/enums live beyond single-file scope: reads of
    // them resolve to nothing. Writing `p.px` also *reads* `p` itself --
    // the object is evaluated -- so `p` stays clean despite the field write.
    let src = "struct P { int px; };\nstatic int g = 4;\nint main(void) {\n  int orphan;\n  struct P p;\n  p.px = g;\n  return g;\n}";
    assert_eq!(used(Lang::C, src), Vec::<String>::new());
    assert_eq!(dead(Lang::C, src), vec!["orphan".to_string()]);
}
#[test]
fn cpp_pure_literal_flags_while_lambda_and_field_reads_do_not() {
    // the lambda is an impure RHS (never a candidate); `local` is a true
    // inline suggestion; `width()` calls stay member slots, not locals
    let src = "class W { public:\n  int width() { return f; }\n  int f;\n };\nint main() {\n  auto lam = [](){ return 1; };\n  W wt;\n  int local = 3;\n  return lam() + wt.width() + local;\n}";
    assert_eq!(used(Lang::Cpp, src), vec!["local".to_string()]);
    assert_eq!(dead(Lang::Cpp, src), Vec::<String>::new());
}

#[test]
fn header_extension_uses_cpp_so_class_fields_stay_clean() {
    // `.h` must not pick the C grammar: that misreads `class` bodies as
    // functions and NeverUsed-flags every member.
    assert_eq!(
        crate::paths::lang_for(std::path::Path::new("MainWindowPrivate.h")),
        Lang::Cpp
    );
    let src = "class MainWindowPrivate {\npublic:\n  int life;\n  QWidget *presence;\n  Arena *arena = nullptr;\n};\n";
    assert_eq!(dead(Lang::Cpp, src), Vec::<String>::new());
}

#[test]
fn export_macro_class_is_not_a_function_of_locals() {
    // `class UTIL_EXPORT Foo { ... }` is misparsed as a function_definition;
    // members (and a phantom empty declarator on `class Impl;`) must not
    // surface as NeverUsed.
    let src = "class UTIL_EXPORT SaxParser {\nprivate:\n  class Impl;\n  std::unique_ptr<Impl> m_impl;\nprotected:\n  bool m_processed { true };\n};\n";
    assert_eq!(dead(Lang::Cpp, src), Vec::<String>::new());
    assert_eq!(used(Lang::Cpp, src), Vec::<String>::new());
}

#[test]
fn cpp_ifdef_else_if_does_not_bind_if_keyword() {
    // Preprocessor-split `if` / `else if` parses `if` as a declarator;
    // keyword bind filter must refuse it (ifdef bodies are walked for
    // real reads). Keep `dead` before the broken `else` — that misparse
    // can close the function early in the CST.
    let src = "void f(int sock) {\n  int dead = 1;\n  if (sock == 1) { return; }\n#ifdef WITH_UTP\n  else if (sock == 2) { return; }\n#endif\n  else { return; }\n}\n";
    assert_eq!(dead(Lang::Cpp, src), vec!["dead".to_string()]);
}

#[test]
fn cpp_raii_lifetime_guards_are_not_never_used() {
    let src = "void f(Session* session, std::mutex& m, addrinfo* info, QMutex* qm, CriticalSection& cs) {\n  auto const lock = session->unique_lock();\n  auto const held = cm->lock();\n  std::lock_guard const guard{ m };\n  QMutexLocker qlock(qm);\n  HashManager::HashPauser pauser;\n  Lock l(cs);\n  FastLock fl(cs);\n  File f(target, File::WRITE, File::CREATE);\n  auto const keep_alive = shared_from_this();\n  auto const blocker = QSignalBlocker{ w };\n  auto const info_uniq = std::unique_ptr<addrinfo, decltype(&freeaddrinfo)>{ info, freeaddrinfo };\n  int dead = 1;\n}\n";
    assert_eq!(dead(Lang::Cpp, src), vec!["dead".to_string()]);
}

#[test]
fn cpp_block_ctor_style_args_count_as_reads() {
    // `QFile f(path)` parses as function_declarator; params are expression args.
    let src = "void g() {\n  QString path = x();\n  QFile f(path);\n  (void)f;\n  int dead = 1;\n}\n";
    assert_eq!(dead(Lang::Cpp, src), vec!["dead".to_string()]);
}

#[test]
fn cpp_qt_emit_and_debug_block_are_not_locals() {
    // `emit sig(x)` and `DEBUG_BLOCK\\n call()` parse as declarations whose
    // type is the macro; refuse the bind and still count arg reads. A single
    // use of `status` via emit is UsedOnce (real), not NeverUsed (the old FP).
    let src = "void f(Hubs& hubs) {\n  auto it = hubs.find(url);\n  if (it != hubs.end()) {\n    emit hubUnregistered(it.value());\n    hubs.erase(it);\n  }\n  QString status = tr(\"hi\");\n  emit coreConnecting(status);\n  DEBUG_BLOCK\n  setAcceptDrops(true);\n  int dead = 1;\n}\n";
    assert_eq!(dead(Lang::Cpp, src), vec!["dead".to_string()]);
    assert_eq!(used(Lang::Cpp, src), vec!["status".to_string()]);
}

#[test]
fn cpp_ifdef_orphaned_else_does_not_bind_following_name() {
    // `#ifdef` between `if` and `else` makes `else stmt` a declaration with
    // type `else` — do not bind `notify` / `fprintf` as locals.
    let src = "void f(Module* notify) {\n  if (t == QtNotify)\n    notify = new A();\n#ifdef DBUS\n  else\n    notify = new B();\n#endif\n  int dead = 1;\n}\n";
    assert_eq!(dead(Lang::Cpp, src), vec!["dead".to_string()]);
}

#[test]
fn cpp_adjacent_macros_and_index_assign_are_not_locals() {
    // Error recovery: `paths[I] = a + "x" MACRO MACRO` splits into a fake
    // structured_binding decl and `MACRO MACRO;`.
    let src = "void f(string* paths) {\n  paths[PATH_LOCALE] = linExecutablePath() + \"/../../\" LOCALE_DIR PATH_SEPARATOR_STR;\n  int dead = 1;\n}\n";
    assert_eq!(dead(Lang::Cpp, src), vec!["dead".to_string()]);
}

#[test]
fn cpp_template_arg_reads_constexpr_local() {
    // Non-type template arguments are `type_identifier` in tree-sitter-cpp;
    // counting them as reads clears the NeverUsed false positive on BufSize.
    let src = "template<std::size_t N, class T> struct StackBuffer { T data[N]; };\nvoid f() {\n  static auto constexpr BufSize = 32U;\n  auto outbuf = StackBuffer<BufSize, char>{};\n  (void)outbuf;\n}\n";
    assert_eq!(dead(Lang::Cpp, src), Vec::<String>::new());
    assert_eq!(used(Lang::Cpp, src), vec!["BufSize".to_string()]);
}

#[test]
fn cpp_nullability_macro_is_not_a_local() {
    let src = "void f(void) {\n  NS_ASSUME_NONNULL_END;\n  int dead = 1;\n}\n";
    assert_eq!(dead(Lang::Cpp, src), vec!["dead".to_string()]);
}

#[test]
fn cpp_pointer_const_declarator_still_walks_rhs_reads() {
    // `* const ptr` must resolve via @declarator, not named_child(0) (the
    // const qualifier), or the RHS is dropped and outer locals look dead.
    let src = "bool f(char const* address) {\n  auto native = std::string{};\n  auto const* const addr = std::empty(native) ? address : native.c_str();\n  return addr != nullptr;\n}";
    assert_eq!(dead(Lang::Cpp, src), Vec::<String>::new());
}

#[test]
fn cpp_for_init_reads_outer_locals_without_binding_loop_var() {
    let src = "void f() {\n  auto const& spans = getSpans();\n  for (auto it = spans.rbegin(); it != spans.rend(); ++it) { use(*it); }\n}\n";
    assert_eq!(dead(Lang::Cpp, src), Vec::<String>::new());
    assert_eq!(used(Lang::Cpp, src), Vec::<String>::new());
}

#[test]
fn cpp_if_init_condition_counts_as_a_use() {
    let src = "void f(Task* task) {\n  if (auto const range = task->range()) { (void)0; }\n  int dead = 1;\n}\n";
    assert_eq!(dead(Lang::Cpp, src), vec!["dead".to_string()]);
}

#[test]
fn cpp_if_init_pointer_rhs_reads_outer_locals() {
    // `if (T* x = call(priv))` puts `@value` on the declaration (no
    // init_declarator); args must still count as reads.
    let src = "bool f() {\n  const bool priv = isPrivate();\n  if (OnlineUser* ou = findBest(priv)) { return true; }\n  return false;\n}\n";
    assert_eq!(dead(Lang::Cpp, src), Vec::<String>::new());
}

#[test]
fn cpp_reads_inside_ifdef_still_count() {
    let src = "void f() {\n  auto const abs = path();\n#if defined(A)\n  use(abs);\n#endif\n}\n";
    assert_eq!(dead(Lang::Cpp, src), Vec::<String>::new());
}

#[test]
fn cpp_reference_alias_written_later_is_not_never_used() {
    let src = "void f(int& a, int& b, int dir) {\n  auto& tgt = dir == 0 ? a : b;\n  tgt = 1;\n  int dead = 1;\n}\n";
    assert_eq!(dead(Lang::Cpp, src), vec!["dead".to_string()]);
}