#!/usr/bin/env python3 """ Tests for code_analyzer.py - Code analysis at configurable depth levels. Test Coverage: - Python AST parsing (docstrings, signatures, decorators) - JavaScript/TypeScript regex parsing - C++ regex parsing - Depth level behavior (surface/deep) - Error handling """ import os import sys import unittest # Add src to path sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src")) from skill_seekers.cli.code_analyzer import CodeAnalyzer class TestPythonParsing(unittest.TestCase): """Tests for Python AST parsing""" def setUp(self): """Set up test analyzer with deep analysis""" self.analyzer = CodeAnalyzer(depth="deep") def test_python_function_signature_basic(self): """Test basic Python function signature extraction.""" code = ''' def greet(name, age): """Say hello.""" return f"Hello {name}, you are {age}" ''' result = self.analyzer.analyze_file("test.py", code, "Python") self.assertIn("functions", result) self.assertEqual(len(result["functions"]), 1) func = result["functions"][0] self.assertEqual(func["name"], "greet") self.assertEqual(len(func["parameters"]), 2) self.assertEqual(func["parameters"][0]["name"], "name") self.assertEqual(func["parameters"][1]["name"], "age") self.assertEqual(func["docstring"], "Say hello.") def test_python_function_with_type_hints(self): """Test Python function with type annotations.""" code = ''' def add_numbers(a: int, b: int) -> int: """Add two integers.""" return a + b ''' result = self.analyzer.analyze_file("test.py", code, "Python") self.assertIn("functions", result) func = result["functions"][0] self.assertEqual(func["name"], "add_numbers") self.assertEqual(func["return_type"], "int") self.assertEqual(func["parameters"][0]["type_hint"], "int") self.assertEqual(func["parameters"][1]["type_hint"], "int") self.assertEqual(func["docstring"], "Add two integers.") def test_python_function_with_defaults(self): """Test Python function with default parameter values.""" code = ''' def create_user(name: str, age: int = 18, active: bool = True) -> dict: """Create a user object.""" return {"name": name, "age": age, "active": active} ''' result = self.analyzer.analyze_file("test.py", code, "Python") func = result["functions"][0] self.assertEqual(func["name"], "create_user") # Check defaults self.assertIsNone(func["parameters"][0]["default"]) self.assertEqual(func["parameters"][1]["default"], "18") self.assertEqual(func["parameters"][2]["default"], "True") def test_python_async_function(self): """Test async Python function detection.""" code = ''' async def fetch_data(url: str) -> dict: """Fetch data from URL.""" pass ''' result = self.analyzer.analyze_file("test.py", code, "Python") func = result["functions"][0] self.assertEqual(func["name"], "fetch_data") self.assertTrue(func["is_async"]) self.assertEqual(func["return_type"], "dict") def test_python_class_extraction(self): """Test Python class extraction with inheritance.""" code = ''' class Animal: """Base animal class.""" def make_sound(self): """Make a sound.""" pass class Dog(Animal): """Dog class.""" def bark(self): """Bark loudly.""" print("Woof!") ''' result = self.analyzer.analyze_file("test.py", code, "Python") self.assertIn("classes", result) self.assertEqual(len(result["classes"]), 2) # Check first class animal_class = result["classes"][0] self.assertEqual(animal_class["name"], "Animal") self.assertEqual(animal_class["docstring"], "Base animal class.") self.assertEqual(len(animal_class["methods"]), 1) self.assertEqual(animal_class["methods"][0]["name"], "make_sound") # Check inherited class dog_class = result["classes"][1] self.assertEqual(dog_class["name"], "Dog") self.assertEqual(dog_class["base_classes"], ["Animal"]) self.assertEqual(len(dog_class["methods"]), 1) self.assertEqual(dog_class["methods"][0]["name"], "bark") def test_python_docstring_extraction(self): """Test docstring extraction for functions and classes.""" code = ''' class Calculator: """A simple calculator class. Supports basic arithmetic operations. """ def add(self, a, b): """Add two numbers. Args: a: First number b: Second number Returns: Sum of a and b """ return a + b ''' result = self.analyzer.analyze_file("test.py", code, "Python") # Check class docstring calc_class = result["classes"][0] self.assertIn("A simple calculator class", calc_class["docstring"]) self.assertIn("Supports basic arithmetic operations", calc_class["docstring"]) # Check method docstring add_method = calc_class["methods"][0] self.assertIn("Add two numbers", add_method["docstring"]) self.assertIn("Args:", add_method["docstring"]) self.assertIn("Returns:", add_method["docstring"]) def test_python_decorators(self): """Test decorator extraction.""" code = ''' class MyClass: @property def value(self): """Get value.""" return self._value @staticmethod def helper(): """Static helper.""" pass @classmethod def from_dict(cls, data): """Create from dict.""" pass ''' result = self.analyzer.analyze_file("test.py", code, "Python") my_class = result["classes"][0] methods = my_class["methods"] # Check decorators self.assertIn("property", methods[0]["decorators"]) self.assertIn("staticmethod", methods[1]["decorators"]) self.assertIn("classmethod", methods[2]["decorators"]) def test_python_syntax_error_handling(self): """Test handling of malformed Python code.""" code = """ def broken_function( # Missing closing parenthesis return "broken" """ result = self.analyzer.analyze_file("test.py", code, "Python") # Should return empty dict or handle gracefully, not crash self.assertIsInstance(result, dict) # No functions should be extracted from broken code self.assertEqual(result.get("functions", []), []) class TestJavaScriptParsing(unittest.TestCase): """Tests for JavaScript/TypeScript regex parsing""" def setUp(self): """Set up test analyzer with deep analysis""" self.analyzer = CodeAnalyzer(depth="deep") def test_javascript_function_basic(self): """Test basic JavaScript function extraction.""" code = """ function greet(name, age) { return `Hello ${name}, you are ${age}`; } """ result = self.analyzer.analyze_file("test.js", code, "JavaScript") self.assertIn("functions", result) func = result["functions"][0] self.assertEqual(func["name"], "greet") self.assertEqual(len(func["parameters"]), 2) self.assertEqual(func["parameters"][0]["name"], "name") self.assertEqual(func["parameters"][1]["name"], "age") def test_javascript_arrow_function(self): """Test arrow function detection.""" code = """ const add = (a, b) => { return a + b; }; const multiply = (x, y) => x * y; """ result = self.analyzer.analyze_file("test.js", code, "JavaScript") self.assertIn("functions", result) self.assertEqual(len(result["functions"]), 2) # Check first arrow function self.assertEqual(result["functions"][0]["name"], "add") self.assertEqual(len(result["functions"][0]["parameters"]), 2) def test_javascript_class_methods(self): """Test ES6 class method extraction. Note: Regex-based parser has limitations in extracting all methods. This test verifies basic method extraction works. """ code = """ class User { constructor(name, email) { this.name = name; this.email = email; } getProfile() { return { name: this.name, email: this.email }; } async fetchData() { return await fetch('/api/user'); } } """ result = self.analyzer.analyze_file("test.js", code, "JavaScript") self.assertIn("classes", result) user_class = result["classes"][0] self.assertEqual(user_class["name"], "User") # Regex parser may not catch all methods, verify at least one method extracted self.assertGreaterEqual(len(user_class["methods"]), 1) # Check that methods list is not empty method_names = [m["name"] for m in user_class["methods"]] self.assertGreater(len(method_names), 0) def test_javascript_class_methods_after_first_not_dropped(self): """Regression for CBA-03: the class body must be delimited by the MATCHING closing brace, not the first '}', or every method after the first is silently dropped.""" code = """ class Service { constructor() { this.x = 1; } foo() { return this.x; } bar() { return 2; } baz() { return 3; } } """ result = self.analyzer.analyze_file("svc.js", code, "JavaScript") names = {m["name"] for m in result["classes"][0]["methods"]} self.assertTrue({"foo", "bar", "baz"}.issubset(names)) def test_typescript_class_methods_with_return_types_not_dropped(self): """Regression for CBA-14: a TS class method written `name(...): Type {` has a return-type annotation between the params `)` and the body `{`. The method-extraction regex must tolerate it, or every annotated method is silently dropped (and method-count heuristics under-count).""" code = """ class Greeter { greet(name: string): string { return name; } add(a: number, b: number): number { return a + b; } load(): Promise { return Promise.resolve(); } plain() { return 1; } } """ result = self.analyzer.analyze_file("greeter.ts", code, "TypeScript") names = {m["name"] for m in result["classes"][0]["methods"]} self.assertTrue( {"greet", "add", "load", "plain"}.issubset(names), f"TS methods with return types were dropped: got {names}", ) def test_typescript_type_annotations(self): """Test TypeScript type annotation extraction. Note: Current regex-based parser extracts parameter type hints but NOT return types. Return type extraction requires a proper TypeScript parser (ts-morph or typescript library). """ code = """ function calculate(a: number, b: number): number { return a + b; } interface User { name: string; age: number; } function createUser(name: string, age: number = 18): User { return { name, age }; } """ result = self.analyzer.analyze_file("test.ts", code, "TypeScript") self.assertIn("functions", result) # Check first function - parameters extracted, but not return type calc_func = result["functions"][0] self.assertEqual(calc_func["name"], "calculate") self.assertEqual(calc_func["parameters"][0]["type_hint"], "number") # Note: return_type is None because regex parser doesn't extract it self.assertIsNone(calc_func["return_type"]) # Check function with default create_func = result["functions"][1] self.assertEqual(create_func["name"], "createUser") self.assertEqual(create_func["parameters"][1]["default"], "18") # Note: return_type is None (regex parser limitation) self.assertIsNone(create_func["return_type"]) def test_javascript_async_detection(self): """Test async function detection in JavaScript.""" code = """ async function fetchUser(id) { const response = await fetch(`/api/users/${id}`); return response.json(); } const loadData = async () => { return await fetchUser(1); }; """ result = self.analyzer.analyze_file("test.js", code, "JavaScript") self.assertIn("functions", result) self.assertGreaterEqual(len(result["functions"]), 1) # Check async function fetch_func = result["functions"][0] self.assertEqual(fetch_func["name"], "fetchUser") self.assertTrue(fetch_func["is_async"]) class TestCppParsing(unittest.TestCase): """Tests for C++ regex parsing""" def setUp(self): """Set up test analyzer with deep analysis""" self.analyzer = CodeAnalyzer(depth="deep") def test_cpp_function_signature(self): """Test C++ function declaration parsing.""" code = """ int add(int a, int b); std::string getName(); void processData(const std::vector& data); """ result = self.analyzer.analyze_file("test.h", code, "C++") self.assertIn("functions", result) self.assertGreaterEqual(len(result["functions"]), 2) # Check first function add_func = result["functions"][0] self.assertEqual(add_func["name"], "add") self.assertEqual(add_func["return_type"], "int") def test_cpp_class_extraction(self): """Test C++ class extraction with inheritance.""" code = """ class Animal { public: virtual void makeSound() = 0; }; class Dog : public Animal { public: void makeSound() override; void bark(); private: std::string breed; }; """ result = self.analyzer.analyze_file("test.h", code, "C++") self.assertIn("classes", result) self.assertEqual(len(result["classes"]), 2) # Check Animal class animal_class = result["classes"][0] self.assertEqual(animal_class["name"], "Animal") # Check Dog class with inheritance dog_class = result["classes"][1] self.assertEqual(dog_class["name"], "Dog") self.assertIn("Animal", dog_class["base_classes"]) def test_cpp_pointer_parameters(self): """Test C++ function with pointer/reference parameters.""" code = """ void process(int* ptr); void update(const int& value); void transform(std::vector* vec); """ result = self.analyzer.analyze_file("test.h", code, "C++") self.assertIn("functions", result) self.assertGreaterEqual(len(result["functions"]), 2) # Check that parameters include pointer/reference syntax process_func = result["functions"][0] self.assertEqual(process_func["name"], "process") def test_cpp_default_parameters(self): """Test C++ function with default parameter values.""" code = """ void initialize(int size = 100, bool verbose = false); class Config { public: Config(std::string name = "default", int timeout = 30); }; """ result = self.analyzer.analyze_file("test.h", code, "C++") self.assertIn("functions", result) # Check function with defaults init_func = result["functions"][0] self.assertEqual(init_func["name"], "initialize") # Verify defaults are captured self.assertGreaterEqual(len(init_func["parameters"]), 2) class TestDepthLevels(unittest.TestCase): """Tests for depth level behavior""" def test_surface_depth_returns_empty(self): """Test that surface depth returns empty analysis.""" analyzer = CodeAnalyzer(depth="surface") code = ''' def test_function(a, b): """Test.""" return a + b ''' result = analyzer.analyze_file("test.py", code, "Python") # Surface depth should return empty dict self.assertEqual(result, {}) def test_deep_depth_extracts_signatures(self): """Test that deep depth extracts full signatures.""" analyzer = CodeAnalyzer(depth="deep") code = ''' def calculate(x: int, y: int) -> int: """Calculate sum.""" return x + y ''' result = analyzer.analyze_file("test.py", code, "Python") # Deep depth should extract full analysis self.assertIn("functions", result) self.assertEqual(len(result["functions"]), 1) func = result["functions"][0] self.assertEqual(func["name"], "calculate") self.assertEqual(func["return_type"], "int") def test_unknown_language_returns_empty(self): """Test that unknown language returns empty dict.""" analyzer = CodeAnalyzer(depth="deep") code = """ import Foundation func greet(name: String) { print("Hello, \\(name)!") } """ result = analyzer.analyze_file("test.swift", code, "Swift") # Unknown language should return empty dict self.assertEqual(result, {}) class TestIntegration(unittest.TestCase): """Integration tests""" def test_analyze_file_interface(self): """Test the analyze_file public interface.""" analyzer = CodeAnalyzer(depth="deep") # Test with Python code py_code = "def test(): pass" result = analyzer.analyze_file("test.py", py_code, "Python") self.assertIsInstance(result, dict) # Test with JavaScript code js_code = "function test() {}" result = analyzer.analyze_file("test.js", js_code, "JavaScript") self.assertIsInstance(result, dict) # Test with C++ code cpp_code = "void test();" result = analyzer.analyze_file("test.h", cpp_code, "C++") self.assertIsInstance(result, dict) def test_multiple_items_extraction(self): """Test extracting multiple classes and functions.""" analyzer = CodeAnalyzer(depth="deep") code = ''' def helper_func(): """Helper function.""" pass class ClassA: """First class.""" def method_a(self): pass class ClassB: """Second class.""" def method_b(self): pass def main_func(): """Main function.""" pass ''' result = analyzer.analyze_file("test.py", code, "Python") # Should extract 2 standalone functions self.assertEqual(len(result["functions"]), 2) # Should extract 2 classes self.assertEqual(len(result["classes"]), 2) # Verify names func_names = [f["name"] for f in result["functions"]] self.assertIn("helper_func", func_names) self.assertIn("main_func", func_names) class_names = [c["name"] for c in result["classes"]] self.assertIn("ClassA", class_names) self.assertIn("ClassB", class_names) class TestCommentExtraction(unittest.TestCase): """Tests for comment extraction""" def setUp(self): """Set up test analyzer with deep analysis""" self.analyzer = CodeAnalyzer(depth="deep") def test_python_comment_extraction(self): """Test Python # comment extraction.""" code = """ # This is a comment def test_func(): # Inside function comment x = 5 # Inline comment (not extracted due to code on same line) return x # Another top-level comment class TestClass: # Class-level comment pass """ result = self.analyzer.analyze_file("test.py", code, "Python") self.assertIn("comments", result) comments = result["comments"] # Should have extracted standalone comments self.assertGreaterEqual(len(comments), 3) # Check comment content comment_texts = [c["text"] for c in comments] self.assertIn("This is a comment", comment_texts) self.assertIn("Inside function comment", comment_texts) self.assertIn("Another top-level comment", comment_texts) # Check all are inline type for comment in comments: self.assertEqual(comment["type"], "inline") def test_python_comment_line_numbers(self): """Test Python comment line number tracking.""" code = """# Line 1 comment def func(): # Line 3 comment pass # Line 5 comment """ result = self.analyzer.analyze_file("test.py", code, "Python") comments = result["comments"] self.assertEqual(len(comments), 3) # Check line numbers line_nums = [c["line"] for c in comments] self.assertIn(1, line_nums) self.assertIn(3, line_nums) self.assertIn(5, line_nums) def test_python_skip_shebang_and_encoding(self): """Test that shebang and encoding declarations are skipped.""" code = """#!/usr/bin/env python3 # -*- coding: utf-8 -*- # This is a real comment def func(): pass """ result = self.analyzer.analyze_file("test.py", code, "Python") comments = result["comments"] # Should only have the real comment self.assertEqual(len(comments), 1) self.assertEqual(comments[0]["text"], "This is a real comment") def test_javascript_inline_comments(self): """Test JavaScript // comment extraction.""" code = """ // Top-level comment function test() { // Inside function const x = 5; // Inline (not extracted) return x; } // Another comment const y = 10; """ result = self.analyzer.analyze_file("test.js", code, "JavaScript") self.assertIn("comments", result) comments = result["comments"] # Should have extracted standalone comments self.assertGreaterEqual(len(comments), 3) # Check comment types inline_comments = [c for c in comments if c["type"] == "inline"] self.assertGreaterEqual(len(inline_comments), 3) def test_javascript_block_comments(self): """Test JavaScript /* */ block comment extraction.""" code = """ /* This is a multi-line block comment */ function test() { /* Another block comment */ return 42; } """ result = self.analyzer.analyze_file("test.js", code, "JavaScript") comments = result["comments"] # Should have extracted block comments block_comments = [c for c in comments if c["type"] == "block"] self.assertGreaterEqual(len(block_comments), 2) # Check multi-line content is preserved first_block = next(c for c in comments if "multi-line" in c["text"]) self.assertIn("multi-line", first_block["text"]) def test_javascript_mixed_comments(self): """Test JavaScript mixed inline and block comments.""" code = """ // Inline comment /* Block comment */ function test() { // Another inline /* Another block */ return true; } """ result = self.analyzer.analyze_file("test.js", code, "JavaScript") comments = result["comments"] # Should have both types inline_comments = [c for c in comments if c["type"] == "inline"] block_comments = [c for c in comments if c["type"] == "block"] self.assertGreaterEqual(len(inline_comments), 2) self.assertGreaterEqual(len(block_comments), 2) def test_javascript_comments_ignore_markers_inside_strings(self): """Comment markers inside string literals are not comments.""" code = """ const url = "https://example.com/path"; const block = "/* not a block comment */"; const slash = '// not an inline comment'; const template = `https://example.com/${value}/* not a block either */`; /* Real block */ const value = 1; // Real inline """ result = self.analyzer.analyze_file("test.js", code, "JavaScript") comment_texts = [c["text"] for c in result["comments"]] self.assertEqual(comment_texts, ["Real inline", "Real block"]) def test_javascript_comments_ignore_markers_inside_comments(self): """Comment markers inside real comments are not counted twice.""" code = """ /* Block mentions // not an inline comment */ const value = 1; // Inline mentions /* not a block comment */ """ result = self.analyzer.analyze_file("test.js", code, "JavaScript") comment_texts = [c["text"] for c in result["comments"]] self.assertEqual( comment_texts, [ "Inline mentions /* not a block comment */", "Block mentions // not an inline comment", ], ) def test_java_comments_ignore_markers_inside_strings(self): """Java comment extraction ignores string literals but keeps JavaDoc.""" code = """ public class Demo { String url = "https://example.com/path"; String block = "/* not a block comment */"; // Real inline /** Real JavaDoc */ /* Real block */ void run() {} } """ result = self.analyzer.analyze_file("Demo.java", code, "Java") comments = [(c["text"], c["type"]) for c in result["comments"]] self.assertEqual( comments, [ ("Real inline", "inline"), ("Real JavaDoc", "doc"), ("Real block", "block"), ], ) def test_go_raw_string_backslash_does_not_hide_following_comment(self): """Go raw strings use backticks without backslash escaping.""" code = r""" package main func main() { raw := `ends with \` // Real Go comment } """ result = self.analyzer.analyze_file("main.go", code, "Go") comment_texts = [c["text"] for c in result["comments"]] self.assertEqual(comment_texts, ["Real Go comment"]) def test_cpp_digit_separator_does_not_swallow_comments(self): """A single ' digit separator (C++14, e.g. 1'000) is not a char-literal opener.""" code = """ int main() { int x = 1'000; // comment one int y = 5; // comment two } """ result = self.analyzer.analyze_file("a.cpp", code, "C++") comment_texts = [c["text"] for c in result["comments"]] self.assertEqual(comment_texts, ["comment one", "comment two"]) def test_unterminated_block_comment_emits_no_comment(self): """An unterminated /* matches the prior regex behaviour and yields nothing.""" code = "var a = 1;\n/* never closed\nvar b = 2;" result = self.analyzer.analyze_file("x.js", code, "JavaScript") self.assertEqual(result["comments"], []) def test_empty_block_comment_emits_no_comment(self): """An empty /**/ has no inner text and is not reported (matches prior regex).""" code = "public class Demo {\n /**/\n void run() {}\n}\n" result = self.analyzer.analyze_file("Demo.java", code, "Java") self.assertEqual(result["comments"], []) def test_cpp_comment_extraction(self): """Test C++ comment extraction (uses same logic as JavaScript).""" code = """ // Header comment class Node { public: // Method comment void update(); /* Block comment for data member */ int value; }; """ result = self.analyzer.analyze_file("test.h", code, "C++") self.assertIn("comments", result) comments = result["comments"] # Should have extracted comments self.assertGreaterEqual(len(comments), 3) # Check both inline and block inline_comments = [c for c in comments if c["type"] == "inline"] block_comments = [c for c in comments if c["type"] == "block"] self.assertGreaterEqual(len(inline_comments), 2) self.assertGreaterEqual(len(block_comments), 1) def test_todo_fixme_comment_detection(self): """Test that TODO/FIXME comments are extracted.""" code = """ # TODO: Implement this feature def incomplete_func(): # FIXME: Handle edge case pass # NOTE: Important information """ result = self.analyzer.analyze_file("test.py", code, "Python") comments = result["comments"] comment_texts = [c["text"] for c in comments] self.assertTrue(any("TODO" in text for text in comment_texts)) self.assertTrue(any("FIXME" in text for text in comment_texts)) self.assertTrue(any("NOTE" in text for text in comment_texts)) class TestCSharpParsing(unittest.TestCase): """Tests for C# code analysis""" def setUp(self): self.analyzer = CodeAnalyzer(depth="deep") def test_csharp_class_extraction(self): """Test C# class extraction with inheritance.""" code = """ using System; public class PlayerController : MonoBehaviour { private float speed = 5f; } """ result = self.analyzer.analyze_file("test.cs", code, "C#") self.assertIn("classes", result) self.assertEqual(len(result["classes"]), 1) cls = result["classes"][0] self.assertEqual(cls["name"], "PlayerController") self.assertIn("MonoBehaviour", cls["base_classes"]) def test_csharp_method_extraction(self): """Test C# method extraction with parameters.""" code = """ public class Calculator { public int Add(int a, int b) { return a + b; } } """ result = self.analyzer.analyze_file("test.cs", code, "C#") self.assertIn("functions", result) self.assertEqual(len(result["functions"]), 1) method = result["functions"][0] self.assertEqual(method["name"], "Add") self.assertEqual(len(method["parameters"]), 2) self.assertEqual(method["return_type"], "int") def test_csharp_property_extraction(self): """Test C# property extraction.""" code = """ public class Player { public int Health { get; set; } = 100; private string Name { get; } } """ result = self.analyzer.analyze_file("test.cs", code, "C#") # Properties are extracted as part of class analysis self.assertIn("classes", result) cls = result["classes"][0] self.assertEqual(cls["name"], "Player") def test_csharp_async_method(self): """Test C# async method detection.""" code = """ public class DataLoader { public async Task LoadDataAsync() { await Task.Delay(100); return "data"; } } """ result = self.analyzer.analyze_file("test.cs", code, "C#") self.assertIn("functions", result) method = result["functions"][0] self.assertEqual(method["name"], "LoadDataAsync") self.assertTrue(method["is_async"]) class TestGoParsing(unittest.TestCase): """Tests for Go code analysis""" def setUp(self): self.analyzer = CodeAnalyzer(depth="deep") def test_go_function_extraction(self): """Test Go function extraction.""" code = """ package main func Add(a int, b int) int { return a + b } """ result = self.analyzer.analyze_file("test.go", code, "Go") self.assertIn("functions", result) self.assertEqual(len(result["functions"]), 1) func = result["functions"][0] self.assertEqual(func["name"], "Add") self.assertEqual(func["return_type"], "int") def test_go_method_with_receiver(self): """Test Go method with receiver.""" code = """ package main type Person struct { Name string } func (p *Person) Greet() string { return "Hello " + p.Name } """ result = self.analyzer.analyze_file("test.go", code, "Go") self.assertIn("functions", result) # Should extract method method = next((f for f in result["functions"] if f["name"] == "Greet"), None) self.assertIsNotNone(method) self.assertEqual(method["return_type"], "string") def test_go_struct_extraction(self): """Test Go struct extraction.""" code = """ package main type Rectangle struct { Width float64 Height float64 } """ result = self.analyzer.analyze_file("test.go", code, "Go") self.assertIn("classes", result) self.assertEqual(len(result["classes"]), 1) struct = result["classes"][0] self.assertEqual(struct["name"], "Rectangle") def test_go_multiple_return_values(self): """Test Go function with multiple return values.""" code = """ func Divide(a, b float64) (float64, error) { if b == 0 { return 0, errors.New("division by zero") } return a / b, nil } """ result = self.analyzer.analyze_file("test.go", code, "Go") self.assertIn("functions", result) func = result["functions"][0] self.assertEqual(func["name"], "Divide") class TestRustParsing(unittest.TestCase): """Tests for Rust code analysis""" def setUp(self): self.analyzer = CodeAnalyzer(depth="deep") def test_rust_function_extraction(self): """Test Rust function extraction.""" code = """ pub fn add(a: i32, b: i32) -> i32 { a + b } """ result = self.analyzer.analyze_file("test.rs", code, "Rust") self.assertIn("functions", result) self.assertEqual(len(result["functions"]), 1) func = result["functions"][0] self.assertEqual(func["name"], "add") self.assertEqual(func["return_type"], "i32") def test_rust_struct_extraction(self): """Test Rust struct extraction.""" code = """ pub struct Point { x: f64, y: f64, } """ result = self.analyzer.analyze_file("test.rs", code, "Rust") self.assertIn("classes", result) self.assertEqual(len(result["classes"]), 1) struct = result["classes"][0] self.assertEqual(struct["name"], "Point") def test_rust_async_function(self): """Test Rust async function detection.""" code = """ pub async fn fetch_data() -> Result { Ok("data".to_string()) } """ result = self.analyzer.analyze_file("test.rs", code, "Rust") self.assertIn("functions", result) func = result["functions"][0] self.assertEqual(func["name"], "fetch_data") self.assertTrue(func["is_async"]) def test_rust_impl_block(self): """Test Rust impl block method extraction.""" code = """ struct Circle { radius: f64, } impl Circle { pub fn area(&self) -> f64 { std::f64::consts::PI * self.radius * self.radius } } """ result = self.analyzer.analyze_file("test.rs", code, "Rust") self.assertIn("classes", result) self.assertIn("functions", result) class TestJavaParsing(unittest.TestCase): """Tests for Java code analysis""" def setUp(self): self.analyzer = CodeAnalyzer(depth="deep") def test_java_class_extraction(self): """Test Java class extraction with inheritance.""" code = """ public class ArrayList extends AbstractList implements List { private int size; } """ result = self.analyzer.analyze_file("test.java", code, "Java") self.assertIn("classes", result) self.assertEqual(len(result["classes"]), 1) cls = result["classes"][0] self.assertEqual(cls["name"], "ArrayList") self.assertIn("AbstractList", cls["base_classes"]) def test_java_method_extraction(self): """Test Java method extraction.""" code = """ public class Calculator { public static int multiply(int a, int b) { return a * b; } } """ result = self.analyzer.analyze_file("test.java", code, "Java") self.assertIn("functions", result) self.assertEqual(len(result["functions"]), 1) method = result["functions"][0] self.assertEqual(method["name"], "multiply") self.assertEqual(method["return_type"], "int") def test_java_interface_implementation(self): """Test Java interface implementation.""" code = """ public class MyHandler implements EventHandler, Runnable { public void run() {} } """ result = self.analyzer.analyze_file("test.java", code, "Java") self.assertIn("classes", result) cls = result["classes"][0] self.assertEqual(cls["name"], "MyHandler") def test_java_generic_class(self): """Test Java generic class.""" code = """ public class Box { private T value; public T getValue() { return value; } } """ result = self.analyzer.analyze_file("test.java", code, "Java") self.assertIn("classes", result) self.assertIn("functions", result) class TestRubyParsing(unittest.TestCase): """Tests for Ruby code analysis""" def setUp(self): self.analyzer = CodeAnalyzer(depth="deep") def test_ruby_class_extraction(self): """Test Ruby class extraction.""" code = """ class Person def initialize(name) @name = name end end """ result = self.analyzer.analyze_file("test.rb", code, "Ruby") self.assertIn("classes", result) self.assertEqual(len(result["classes"]), 1) cls = result["classes"][0] self.assertEqual(cls["name"], "Person") def test_ruby_method_extraction(self): """Test Ruby method extraction.""" code = """ def greet(name) puts "Hello, #{name}!" end """ result = self.analyzer.analyze_file("test.rb", code, "Ruby") self.assertIn("functions", result) self.assertEqual(len(result["functions"]), 1) method = result["functions"][0] self.assertEqual(method["name"], "greet") def test_ruby_class_inheritance(self): """Test Ruby class inheritance.""" code = """ class Dog < Animal def bark puts "Woof!" end end """ result = self.analyzer.analyze_file("test.rb", code, "Ruby") self.assertIn("classes", result) cls = result["classes"][0] self.assertEqual(cls["name"], "Dog") self.assertIn("Animal", cls["base_classes"]) def test_ruby_predicate_methods(self): """Test Ruby predicate methods (ending with ?).""" code = """ def empty? @items.length == 0 end """ result = self.analyzer.analyze_file("test.rb", code, "Ruby") self.assertIn("functions", result) method = result["functions"][0] self.assertEqual(method["name"], "empty?") class TestPHPParsing(unittest.TestCase): """Tests for PHP code analysis""" def setUp(self): self.analyzer = CodeAnalyzer(depth="deep") def test_php_class_extraction(self): """Test PHP class extraction.""" code = """ name; } } ?> """ result = self.analyzer.analyze_file("test.php", code, "PHP") self.assertIn("classes", result) self.assertEqual(len(result["classes"]), 1) cls = result["classes"][0] self.assertEqual(cls["name"], "User") def test_php_method_extraction(self): """Test PHP method extraction.""" code = """ """ result = self.analyzer.analyze_file("test.php", code, "PHP") self.assertIn("functions", result) self.assertEqual(len(result["functions"]), 1) func = result["functions"][0] self.assertEqual(func["name"], "calculate") def test_php_class_inheritance(self): """Test PHP class inheritance and interfaces.""" code = """ """ result = self.analyzer.analyze_file("test.php", code, "PHP") self.assertIn("classes", result) cls = result["classes"][0] self.assertEqual(cls["name"], "Rectangle") self.assertIn("Shape", cls["base_classes"]) def test_php_namespace(self): """Test PHP namespace handling.""" code = """ """ result = self.analyzer.analyze_file("test.php", code, "PHP") self.assertIn("classes", result) cls = result["classes"][0] self.assertEqual(cls["name"], "Product") class TestEdgeCases(unittest.TestCase): def setUp(self): from skill_seekers.cli.code_analyzer import CodeAnalyzer self.analyzer = CodeAnalyzer(depth="deep") def test_empty_file(self): result = self.analyzer.analyze_file("empty.py", "", "Python") if result: self.assertIn("classes", result) def test_whitespace_only(self): result = self.analyzer.analyze_file("ws.py", "\n\n \n", "Python") if result: self.assertIn("classes", result) def test_comments_only(self): code = "# This is a comment\n# Another comment\n" result = self.analyzer.analyze_file("comments.py", code, "Python") self.assertIsNotNone(result) self.assertGreater(len(result.get("comments", [])), 0) def test_walrus_operator(self): code = "def process(data):\n if (n := len(data)) > 10:\n return f'L: {n}'\n return f'S: {n}'\n" result = self.analyzer.analyze_file("walrus.py", code, "Python") if result: self.assertIn("functions", result) def test_syntax_error(self): result = self.analyzer.analyze_file("broken.py", "def broken(", "Python") self.assertIsInstance(result, dict) def test_swift_language(self): code = "struct Person { var name: String; func greet() -> String { return 'Hello' } }" result = self.analyzer.analyze_file("Person.swift", code, "Swift") if result: self.assertIn("classes", result) def test_dart_language(self): code = "class Calc { double add(double a, double b) => a + b; }" result = self.analyzer.analyze_file("calc.dart", code, "Dart") if result: self.assertIn("classes", result) def test_data_class(self): code = "from dataclasses import dataclass\n@dataclass\nclass Point:\n x: float\n y: float\n def distance(self) -> float:\n return (self.x ** 2 + self.y ** 2) ** 0.5\n" result = self.analyzer.analyze_file("point.py", code, "Python") if result: classes = result.get("classes", []) if classes: self.assertEqual(classes[0]["name"], "Point") class TestUnknownLanguage(unittest.TestCase): def setUp(self): from skill_seekers.cli.code_analyzer import CodeAnalyzer self.analyzer = CodeAnalyzer(depth="deep") def test_unknown_language_surface(self): self.analyzer.depth = "surface" result = self.analyzer.analyze_file("test.xyz", "some content", "xyz") self.assertIsNotNone(result) def test_unknown_language_deep(self): result = self.analyzer.analyze_file("test.xyz", "some content", "xyz") self.assertIsInstance(result, dict) class TestGDScriptParsing(unittest.TestCase): """Tests for GDScript parsing (regression for CBA-13).""" def setUp(self): self.analyzer = CodeAnalyzer(depth="deep") def test_gdscript_extends_populates_base_classes(self): """class_name ... extends ... must expose 'base_classes' (not 'bases'). PatternRecognizer._convert_to_signatures reads cls['base_classes']; using the wrong key silently zeroes all GDScript inheritance-based pattern detection. """ code = "class_name PlayerController extends CharacterBody2D\n\nfunc _ready():\n pass\n" result = self.analyzer.analyze_file("player.gd", code, "GDScript") classes = result["classes"] self.assertEqual(len(classes), 1) cls = classes[0] self.assertEqual(cls["name"], "PlayerController") self.assertIn("base_classes", cls) self.assertEqual(cls["base_classes"], ["CharacterBody2D"]) self.assertNotIn("bases", cls) def test_gdscript_no_extends_has_empty_base_classes(self): code = "class_name Utils\n\nfunc helper():\n pass\n" result = self.analyzer.analyze_file("utils.gd", code, "GDScript") cls = result["classes"][0] self.assertEqual(cls["base_classes"], []) class TestKotlinSuspendModifier(unittest.TestCase): """Regression: is_suspend must detect the `suspend` MODIFIER only, not `suspend` appearing inside the parameter list.""" def setUp(self): self.analyzer = CodeAnalyzer(depth="deep") def test_suspend_lambda_parameter_is_not_async(self): """`fun launchJob(block: suspend () -> Unit)` is NOT a suspend function: `suspend` there is part of the parameter's function type. Searching the whole regex match (which includes the parameter list) mis-flagged it.""" code = ( "fun launchJob(block: suspend () -> Unit) {\n block()\n}\n\n" "suspend fun doWork(): Int {\n return 1\n}\n" ) result = self.analyzer.analyze_file("Jobs.kt", code, "Kotlin") funcs = {f["name"]: f for f in result["functions"]} self.assertFalse(funcs["launchJob"]["is_async"]) self.assertTrue(funcs["doWork"]["is_async"]) def test_suspend_with_other_modifiers_still_detected(self): """Canonical Kotlin modifier order puts `suspend` before `inline`, so the modifier check must not require `suspend` adjacent to `fun`.""" code = "private suspend inline fun fastWork(x: Int): Int {\n return x\n}\n" result = self.analyzer.analyze_file("Fast.kt", code, "Kotlin") funcs = {f["name"]: f for f in result["functions"]} self.assertTrue(funcs["fastWork"]["is_async"]) if __name__ == "__main__": # Run tests with verbose output unittest.main(verbosity=2)