use std::collections::BTreeMap;
use std::path::Path;
use sloc_languages::{
AnalysisOptions, Language, ParseMode, analyze_text, detect_language, looks_like_cpp,
};
#[test]
fn cpp_function_variable_scope_and_macro_detection() {
let src = "\
#define MAX_LEN 128
namespace app {
int g_counter = 0;
class Widget {
int member_field;
std::string label;
public:
MyType compute(int a, int b) {
int local_sum = a + b;
return local_sum;
}
};
std::string make_label(const Widget& w) {
std::string s = \"x\";
return s;
}
}
";
let r = analyze_text(Language::Cpp, src, AnalysisOptions::default());
assert_eq!(r.raw.macro_definitions, 1, "MAX_LEN macro should count");
assert!(
r.raw.functions >= 2,
"expected >=2 functions, got {}",
r.raw.functions
);
assert!(r.raw.classes >= 1, "Widget class should be counted");
assert!(
r.raw.variables_member >= 1,
"member field expected, got {}",
r.raw.variables_member
);
assert!(
r.raw.variables_local >= 1,
"local var expected, got {}",
r.raw.variables_local
);
assert!(
r.raw.variables_global >= 1,
"global var expected, got {}",
r.raw.variables_global
);
assert_eq!(
r.raw.variables,
r.raw.variables_member + r.raw.variables_local + r.raw.variables_global,
"flat total must equal the scope breakdown sum"
);
}
#[test]
fn cpp_most_vexing_parse_direct_init_is_not_a_function() {
let var = "std::istringstream ss(input_string);\n";
let r = analyze_text(Language::Cpp, var, AnalysisOptions::default());
assert_eq!(
r.raw.functions, 0,
"direct-init variable must not be a function"
);
let proto = "int parse(int argc, char** argv);\n";
let r2 = analyze_text(Language::Cpp, proto, AnalysisOptions::default());
assert_eq!(r2.raw.functions, 1, "prototype should count as a function");
}
#[test]
fn cpp_calls_and_control_flow_are_not_functions() {
let src = "\
void run() {
foo(x);
obj->method(y);
if (a && b) {
do_work();
}
}
";
let r = analyze_text(Language::Cpp, src, AnalysisOptions::default());
assert_eq!(
r.raw.functions, 1,
"only run() is a definition, got {}",
r.raw.functions
);
}
#[test]
fn cpp_braces_in_strings_and_comments_do_not_break_scope() {
let src = "\
void f() {
const char* s = \"a { b } c\";
char c = '}';
int x = 1; /* inline { comment } */
int y = 2;
}
";
let r = analyze_text(Language::Cpp, src, AnalysisOptions::default());
assert!(
r.raw.variables_local >= 2,
"expected >=2 locals, got {}",
r.raw.variables_local
);
}
#[test]
fn looks_like_cpp_detects_markers_and_rejects_plain_c() {
assert!(looks_like_cpp("namespace app {\n"));
assert!(looks_like_cpp("auto x = std::make_unique<T>();\n"));
assert!(looks_like_cpp("constexpr int k = 3;\n"));
assert!(!looks_like_cpp("int add(int a, int b) { return a + b; }\n"));
}
#[test]
fn python_single_quote_docstring_and_decorator() {
let src = "\
@decorator
def greet():
'''single-quote docstring'''
return 1
";
let r = analyze_text(Language::Python, src, AnalysisOptions::default());
assert!(
r.raw.docstring_comment_lines >= 1,
"single-quote docstring should be counted, got {}",
r.raw.docstring_comment_lines
);
assert!(r.raw.functions >= 1, "greet() should count as a function");
}
#[test]
fn python_unclosed_docstring_at_eof_is_best_effort() {
let src = "\
def f():
\"\"\"this docstring never closes
still going
";
let r = analyze_text(Language::Python, src, AnalysisOptions::default());
assert!(
r.raw.docstring_comment_lines >= 2,
"remaining lines after an unclosed docstring should be marked, got {}",
r.raw.docstring_comment_lines
);
}
#[test]
fn cpp_unclosed_block_comment_is_best_effort() {
let src = "int x = 1;\n/* this comment never closes\nstill inside\n";
let r = analyze_text(Language::Cpp, src, AnalysisOptions::default());
assert!(
matches!(r.parse_mode, ParseMode::LexicalBestEffort),
"unclosed block comment must be best-effort"
);
assert!(
r.warnings.iter().any(|w| w.contains("block comment")),
"should warn about the unclosed block comment: {:?}",
r.warnings
);
}
#[test]
fn cpp_unclosed_string_literal_is_best_effort() {
let src = "const char* s = \"unterminated\nint y = 2;\n";
let r = analyze_text(Language::Cpp, src, AnalysisOptions::default());
assert!(
r.warnings.iter().any(|w| w.contains("string literal")),
"should warn about the unclosed string literal: {:?}",
r.warnings
);
}
#[test]
fn c_continuation_line_flushed_at_eof() {
let opts = AnalysisOptions {
collapse_continuation_lines: true,
..AnalysisOptions::default()
};
let src = "int total = a + \\\n b + \\\n c";
let r = analyze_text(Language::C, src, opts);
assert!(
r.raw.code_only_lines >= 1,
"collapsed continuation should record code, got {}",
r.raw.code_only_lines
);
}
#[test]
fn detect_language_extension_override_wins() {
let mut overrides = BTreeMap::new();
overrides.insert("rrs".to_string(), "Rust".to_string());
let lang = detect_language(Path::new("weird.rrs"), None, &overrides, true);
assert_eq!(
lang,
Some(Language::Rust),
"override should map .rrs to Rust"
);
}
#[test]
fn detect_language_shebang_node_and_unknown() {
let empty = BTreeMap::new();
let js = detect_language(
Path::new("script"),
Some("#!/usr/bin/env node"),
&empty,
true,
);
assert_eq!(js, Some(Language::JavaScript), "node shebang → JavaScript");
let unknown = detect_language(
Path::new("script"),
Some("#!/usr/bin/env frobnicate"),
&empty,
true,
);
assert_eq!(unknown, None, "unknown interpreter → no language");
}
#[test]
fn xml_has_no_symbol_counts() {
let r = analyze_text(
Language::Xml,
"<!-- doc -->\n<root>\n <child/>\n</root>\n",
AnalysisOptions::default(),
);
assert_eq!(r.raw.functions, 0);
assert_eq!(r.raw.classes, 0);
assert!(r.raw.code_only_lines >= 1, "XML markup lines are code");
}