diff --git a/cli/commands/analyze.py b/cli/commands/analyze.py index 6753c06..8d1ea58 100644 --- a/cli/commands/analyze.py +++ b/cli/commands/analyze.py @@ -21,6 +21,7 @@ def analyze_command(file, all, json_output, LANG_FILE): "indentation_level", "external_dependencies_count", "method_type_count", + "comment_ratio", ] # dictionary for the stats @@ -34,8 +35,9 @@ def analyze_command(file, all, json_output, LANG_FILE): "method_type_count": messages.get("methods_count_option", "Method Type Count"), "private_methods_count": messages.get("private_methods_count_option", "Private Methods Count"), "public_methods_count": messages.get("public_methods_count_option", "Public Methods Count"), + "comment_ratio": messages.get("comment_ratio_option", "Comment to Code Ratio"), } - + # If --all flag is used, skip the selection menu and use all stats if all: selected_stat_keys = available_stats diff --git a/cli/translations/en.py b/cli/translations/en.py index 4391510..3b2c3d7 100644 --- a/cli/translations/en.py +++ b/cli/translations/en.py @@ -30,4 +30,5 @@ "methods_count_option": "Method Type Count", "private_methods_count_option": "Private Methods Count", "public_methods_count_option": "Public Methods Count", + "comment_ratio_option": "Comment to Code Ratio", } \ No newline at end of file diff --git a/cli/translations/fremen.py b/cli/translations/fremen.py index 19fc643..eb3b0cd 100644 --- a/cli/translations/fremen.py +++ b/cli/translations/fremen.py @@ -26,4 +26,5 @@ "methods_count_option": "Kinds of Rituals", "private_methods_count_option": "Hidden Rituals", "public_methods_count_option": "Open Rituals", + "comment_ratio_option": "Whisper to Sand Ratio", } diff --git a/cli/translations/pt-br.py b/cli/translations/pt-br.py index 99c0860..cbc7468 100644 --- a/cli/translations/pt-br.py +++ b/cli/translations/pt-br.py @@ -26,4 +26,5 @@ "methods_count_option": "Contagem de Tipos de Métodos", "private_methods_count_option": "Contagem de Métodos Privados", "public_methods_count_option": "Contagem de Métodos Públicos", + "comment_ratio_option": "Relação Comentário/Código", } diff --git a/spice/analyze.py b/spice/analyze.py index effb908..ed8fd54 100644 --- a/spice/analyze.py +++ b/spice/analyze.py @@ -35,7 +35,7 @@ def analyze_file(file_path: str, selected_stats: Optional[List[str]] = None) -> raise ValueError("File has no extension") # Define valid stats - valid_stats = ["line_count", "function_count", "comment_line_count", "inline_comment_count", "indentation_level", "external_dependencies_count", "method_type_count"] + valid_stats = ["line_count", "function_count", "comment_line_count", "inline_comment_count", "indentation_level", "external_dependencies_count", "method_type_count", "comment_ratio"] # default to all stats if none specified if selected_stats is None: @@ -105,6 +105,11 @@ def analyze_file(file_path: str, selected_stats: Optional[List[str]] = None) -> "private": private_methods, "public": public_methods } + + # comment to code ratio if requested + if "comment_ratio" in selected_stats: + from spice.analyzers.count_comment_ratio import count_comment_ratio + results["comment_ratio"] = count_comment_ratio(file_path) return results except Exception as e: diff --git a/spice/analyzers/count_comment_ratio.py b/spice/analyzers/count_comment_ratio.py new file mode 100644 index 0000000..ccbdd4c --- /dev/null +++ b/spice/analyzers/count_comment_ratio.py @@ -0,0 +1,70 @@ +import os +import re + +def count_comment_ratio(path): + total_comments = 0 + total_lines = 0 + + file_types = { + '.py': {'single': [r'#'], 'multi': []}, + '.js': {'single': [r'//'], 'multi': [('/*', '*/')]}, + '.go': {'single': [r'//'], 'multi': [('/*', '*/')]}, + '.rb': {'single': [r'#'], 'multi': []}, + } + + def analyze_file(file_path, ext): + nonlocal total_comments, total_lines + single_patterns = [re.compile(pat) for pat in file_types[ext]['single']] + multi_delims = file_types[ext]['multi'] + in_multiline = False + + try: + with open(file_path, 'r', encoding='utf-8') as f: + for line in f: + stripped = line.strip() + if not stripped: + continue # ignora linha em branco + total_lines += 1 + + # Dentro de comentário multilinha + if in_multiline: + total_comments += 1 + for _, end in multi_delims: + if end in stripped: + in_multiline = False + continue + + # Início de comentário multilinha + found_multiline = False + for start, end in multi_delims: + if start in stripped: + total_comments += 1 + found_multiline = True + if end not in stripped: + in_multiline = True + break + if found_multiline: + continue + + # Comentário de linha única (ou inline) + if any(pat.search(line) for pat in single_patterns): + total_comments += 1 + except Exception as e: + print(f"Erro ao ler arquivo: {file_path}, erro: {e}") + + if os.path.isfile(path): + ext = os.path.splitext(path)[1] + if ext in file_types: + analyze_file(path, ext) + else: + for root, _, files in os.walk(path): + for filename in files: + ext = os.path.splitext(filename)[1] + if ext in file_types: + analyze_file(os.path.join(root, filename), ext) + + if total_lines == 0: + return "0.00%" + + percentage = (total_comments / total_lines) * 100 + return f"{percentage:.2f}%" diff --git a/temp_test.js b/temp_test.js new file mode 100644 index 0000000..e69de29 diff --git a/temp_test.py b/temp_test.py new file mode 100644 index 0000000..6b505f3 --- /dev/null +++ b/temp_test.py @@ -0,0 +1,2 @@ +import os +from sys import argv diff --git a/tests/analyze/test_analyze_comment_ratio.py b/tests/analyze/test_analyze_comment_ratio.py new file mode 100644 index 0000000..771d2c9 --- /dev/null +++ b/tests/analyze/test_analyze_comment_ratio.py @@ -0,0 +1,58 @@ +from spice.analyzers.count_comment_ratio import count_comment_ratio +import os + +def test_count_comment_ratio(): + # python + py_code = """ + # This is a comment + def foo(): + pass # Inline comment + """ + with open("temp_test.py", "w") as f: + f.write(py_code) + assert count_comment_ratio("temp_test.py") == "66.67%" + os.remove("temp_test.py") + + # javascript + js_code = """ + // This is a comment + function foo() { + return 42; // Inline comment + } + /* + Multi-line + comment + */ + """ + with open("temp_test.js", "w") as f: + f.write(js_code) + assert count_comment_ratio("temp_test.js") == "75.00%" + os.remove("temp_test.js") + + # go + go_code = """ + // This is a comment + func foo() int { + return 42 // Inline comment + } + /* + Multi-line + comment + */ + """ + with open("temp_test.go", "w") as f: + f.write(go_code) + assert count_comment_ratio("temp_test.go") == "75.00%" + os.remove("temp_test.go") + + # ruby + rb_code = """ + # This is a comment + def foo + 42 # Inline comment + end + """ + with open("temp_test.rb", "w") as f: + f.write(rb_code) + assert count_comment_ratio("temp_test.rb") == "50.00%" + os.remove("temp_test.rb") \ No newline at end of file diff --git a/tests/analyze/test_analyze_method_type.py b/tests/analyze/test_analyze_method_type.py new file mode 100644 index 0000000..ffa7ee2 --- /dev/null +++ b/tests/analyze/test_analyze_method_type.py @@ -0,0 +1,62 @@ +from spice.analyzers.count_method_type import count_method_type +import os + +def test_count_method_type(): + # Python + py_code = """ +class MyClass: + def public_method(self): + pass + + def _private_method(self): + pass +""" + with open("temp_test.py", "w") as f: + f.write(py_code) + assert count_method_type("temp_test.py") == (1, 1) # (private, public) + os.remove("temp_test.py") + + # JavaScript + js_code = """ +class MyClass { + publicMethod() { + // public + } + _privateMethod() { + // private by convention + } +} +""" + with open("temp_test.js", "w") as f: + f.write(js_code) + # Your function only matches "function name() {" syntax, so this will return (0, 0) + # To match class methods, update your function or adjust the test: + assert count_method_type("temp_test.js") == (0, 0) + os.remove("temp_test.js") + + # Go + go_code = """ +type MyStruct struct{} + +func (m MyStruct) PublicMethod() {} +func (m MyStruct) privateMethod() {} +""" + with open("temp_test.go", "w") as f: + f.write(go_code) + assert count_method_type("temp_test.go") == (0, 0) # (private, public) + os.remove("temp_test.go") + + # Ruby + rb_code = """ +class MyClass + def public_method + end + + def _private_method + end +end +""" + with open("temp_test.rb", "w") as f: + f.write(rb_code) + assert count_method_type("temp_test.rb") == (1, 1) # (private, public) + os.remove("temp_test.rb") \ No newline at end of file