diff --git a/cli/commands/analyze.py b/cli/commands/analyze.py index 595afa9..6753c06 100644 --- a/cli/commands/analyze.py +++ b/cli/commands/analyze.py @@ -18,7 +18,9 @@ def analyze_command(file, all, json_output, LANG_FILE): "function_count", "comment_line_count", "inline_comment_count", - "indentation_level" + "indentation_level", + "external_dependencies_count", + "method_type_count", ] # dictionary for the stats @@ -27,7 +29,11 @@ def analyze_command(file, all, json_output, LANG_FILE): "function_count": messages.get("function_count_option", "Function Count"), "comment_line_count": messages.get("comment_line_count_option", "Comment Line Count"), "inline_comment_count": messages.get("inline_comment_count_option", "Inline Comment Count"), - "indentation_level": messages.get("indentation_level_option", "Indentation Analysis") + "indentation_level": messages.get("indentation_level_option", "Indentation Analysis"), + "external_dependencies_count": messages.get("external_dependencies_count_option", "External Dependencies Count"), + "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"), } # If --all flag is used, skip the selection menu and use all stats @@ -78,12 +84,20 @@ def analyze_command(file, all, json_output, LANG_FILE): else: # only print the selected stats in normal mode for stat in selected_stat_keys: + #print(stat) #debug + if stat == "method_type_count" and "method_type_count" in results: + mtc = results["method_type_count"] + print(f"{messages.get('public_methods_count_option', 'Public Methods Count')}: {mtc['public']}") + print(f"{messages.get('private_methods_count_option', 'Private Methods Count')}: {mtc['private']}") + continue + if stat == "indentation_level" and "indentation_type" in results: - print(f"{messages.get('indentation_type', 'Indentation Type')}: {results['indentation_type']}") - print(f"{messages.get('indentation_size', 'Indentation Size')}: {results['indentation_size']}") + print(f"{messages.get('indentation_type', 'Indentation Type')}: {results.get('indentation_type', 'N/A')}") + print(f"{messages.get('indentation_size', 'Indentation Size')}: {results.get('indentation_size', 'N/A')}") + continue + elif stat in results: - print(messages.get(stat, f"{stat.replace('_', ' ').title()}: {{count}}").format(count=results[stat])) - + print(f"{stats_labels[stat]}: {results[stat]}") except Exception as e: if json_output: import json diff --git a/cli/translations/en.py b/cli/translations/en.py index 7c9885a..4391510 100644 --- a/cli/translations/en.py +++ b/cli/translations/en.py @@ -22,5 +22,12 @@ # keys for the version command "version_info": "SpiceCode Version:", "version_not_found": "Version information not found in setup.py", - "setup_not_found": "Error: setup.py not found." + "setup_not_found": "Error: setup.py not found.", + "indentation_level_option": "Indentation Level Analysis", + "indentation_type": "Indentation Type", + "indentation_size": "Indentation Size", + "external_dependencies_count_option": "External Dependency Count", + "methods_count_option": "Method Type Count", + "private_methods_count_option": "Private Methods Count", + "public_methods_count_option": "Public Methods Count", } \ No newline at end of file diff --git a/cli/translations/fremen.py b/cli/translations/fremen.py index f7bb68b..19fc643 100644 --- a/cli/translations/fremen.py +++ b/cli/translations/fremen.py @@ -18,5 +18,12 @@ "inline_comment_count_option": "Passages of Dual Meaning", "no_stats_selected": "No omens were heeded. The analysis fades into the sands.", "confirm_and_analyze": "Seal your fate and analyze", - "checkbox_hint": "(Use space to mark, enter to proceed)" + "checkbox_hint": "(Use space to mark, enter to proceed)", + "indentation_level_option": "Sand Pattern Reading", + "indentation_type": "Kind of Sand Grain", + "indentation_size": "Size of the Sand Grain", + "external_dependencies_count_option": "Grains of Sand Beyound Dune", + "methods_count_option": "Kinds of Rituals", + "private_methods_count_option": "Hidden Rituals", + "public_methods_count_option": "Open Rituals", } diff --git a/cli/translations/pt-br.py b/cli/translations/pt-br.py index c5afdf9..99c0860 100644 --- a/cli/translations/pt-br.py +++ b/cli/translations/pt-br.py @@ -18,5 +18,12 @@ "inline_comment_count_option": "Contagem de Comentários Inline", "no_stats_selected": "Nenhuma estatística selecionada. Análise cancelada.", "confirm_and_analyze": "Confirmar e analisar", - "checkbox_hint": "(Use espaço para selecionar, enter para confirmar)" + "checkbox_hint": "(Use espaço para selecionar, enter para confirmar)", + "indentation_level_option": "Análise de Indentação", + "indentation_type": "Tipo de Indentação", + "indentation_size": "Tamanho da Indentação", + "external_dependencies_count_option": "Contagem de Dependências Externas", + "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", } diff --git a/spice/analyze.py b/spice/analyze.py index ca40aa4..effb908 100644 --- a/spice/analyze.py +++ b/spice/analyze.py @@ -1,7 +1,7 @@ import os from typing import List, Dict, Optional, Union -from spice.analyzers.identation import detect_indentation +from spice.analyzers.indentation import detect_indentation def analyze_file(file_path: str, selected_stats: Optional[List[str]] = None) -> Dict[str, Union[int, str, List[int]]]: """ @@ -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"] + valid_stats = ["line_count", "function_count", "comment_line_count", "inline_comment_count", "indentation_level", "external_dependencies_count", "method_type_count"] # default to all stats if none specified if selected_stats is None: @@ -82,19 +82,29 @@ def analyze_file(file_path: str, selected_stats: Optional[List[str]] = None) -> # indentation analysis if requested if "indentation_level" in selected_stats: - indentation_info = detect_indentation(code) - results["indentation_type"] = indentation_info["indent_type"] - results["indentation_size"] = indentation_info["indent_size"] - results["indentation_levels"] = indentation_info["levels"] - + from spice.analyzers.indentation import detect_indentation + indentation_info = detect_indentation(file_path) + results["indentation_type"] = indentation_info["indentation_type"] + results["indentation_size"] = indentation_info["indentation_size"] + # function count if requested if "function_count" in selected_stats: from spice.analyzers.count_functions import count_functions - from utils.get_lexer import get_lexer_for_file - LexerClass = get_lexer_for_file(file_path) - lexer = LexerClass(source_code=code) # Pass source_code explicitly results["function_count"] = count_functions(file_path) + # external dependencies count if requested + if "external_dependencies_count" in selected_stats: + from spice.analyzers.count_external_dependencies import count_external_dependencies + results["external_dependencies_count"] = count_external_dependencies(file_path) + + # method type count if requested + if "method_type_count" in selected_stats: + from spice.analyzers.count_method_type import count_method_type + private_methods, public_methods = count_method_type(file_path) + results["method_type_count"] = { + "private": private_methods, + "public": public_methods + } return results except Exception as e: diff --git a/spice/analyzers/count_external_dependencies.py b/spice/analyzers/count_external_dependencies.py new file mode 100644 index 0000000..6b717b1 --- /dev/null +++ b/spice/analyzers/count_external_dependencies.py @@ -0,0 +1,36 @@ +import os +import re + +def count_external_dependencies(path): + """Contar o número de dependências externas em um arquivo de exemplo.""" + _, ext = os.path.splitext(path) + with open(path, 'r') as file: + code = file.read() + + if ext == '.py': + # Contar o número de importações + pattern = r'^\s*(import\s+\w+|from\s+\w+\s+import\s+.+)' + matches = re.findall(pattern, code, flags=re.MULTILINE) + return len(matches) + elif ext == '.js': + # Contar o número de importações + require_pattern = r'require\s*\(\s*[\'"][^\'"]+[\'"]\s*\)' + import_pattern = r'^\s*import\s+.*from\s+[\'"][^\'"]+[\'"]' + matches = re.findall(require_pattern, code) + matches += re.findall(import_pattern, code, flags=re.MULTILINE) + return len(matches) + elif ext == ".rb": + pattern = r'^\s*require(_relative)?\s+[\'"][^\'"]+[\'"]' + matches = re.findall(pattern, code, flags=re.MULTILINE) + return len(matches) + elif ext == ".go": + import_block_pattern = r'import\s*\((.*?)\)' + single_import_pattern = r'^\s*import\s+[\'"][^\'"]+[\'"]' + block = re.findall(import_block_pattern, code, flags=re.DOTALL) + count = 0 + for b in block: + count += len(re.findall(r'[\'"][^\'"]+[\'"]', b)) + count += len(re.findall(single_import_pattern, code, flags=re.MULTILINE)) + return count + else: + return 0 \ No newline at end of file diff --git a/spice/analyzers/count_method_type.py b/spice/analyzers/count_method_type.py new file mode 100644 index 0000000..e750476 --- /dev/null +++ b/spice/analyzers/count_method_type.py @@ -0,0 +1,39 @@ +import os +import re + +def count_method_type(path): + """Count the number of private and public methods in a sample file.""" + _, ext = os.path.splitext(path) + with open(path, 'r') as file: + code = file.read() + + if ext == '.py': + # Count the number of private and public methods + pattern = r'^\s*def\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*\(.*?\)\s*:\s*(?:#.*)?$' + matches = re.findall(pattern, code, flags=re.MULTILINE) + private_methods = [m for m in matches if m.startswith('_')] + public_methods = [m for m in matches if not m.startswith('_')] + return len(private_methods), len(public_methods) + elif ext == '.js': + # Count the number of private and public methods + pattern = r'^\s*function\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*\(.*?\)\s*{' + matches = re.findall(pattern, code, flags=re.MULTILINE) + private_methods = [m for m in matches if m.startswith('_')] + public_methods = [m for m in matches if not m.startswith('_')] + return len(private_methods), len(public_methods) + elif ext == ".rb": + # Count the number of private and public methods + pattern = r'^\s*def\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*(?:#.*)?$' + matches = re.findall(pattern, code, flags=re.MULTILINE) + private_methods = [m for m in matches if m.startswith('_')] + public_methods = [m for m in matches if not m.startswith('_')] + return len(private_methods), len(public_methods) + elif ext == ".go": + # Count the number of private and public methods + pattern = r'^\s*func\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*\(.*?\)\s*{' + matches = re.findall(pattern, code, flags=re.MULTILINE) + private_methods = [m for m in matches if m[0].islower()] + public_methods = [m for m in matches if m[0].isupper()] + return len(private_methods), len(public_methods) + else: + return 0, 0 \ No newline at end of file diff --git a/spice/analyzers/identation.py b/spice/analyzers/identation.py deleted file mode 100644 index bb91a27..0000000 --- a/spice/analyzers/identation.py +++ /dev/null @@ -1,30 +0,0 @@ -import re - -def detect_indentation(code): - lines = code.split('\n') - indentation_counts = {'tab': 0, 'space': 0} - indentation_levels = [] - - for line in lines: - if line.strip() == '': - continue # skip empty lines - leading_whitespace = re.match(r'^\s*', line).group() - #detect space, tab or new line within the function - if '\t' in leading_whitespace: - indentation_counts['tab'] += 1 - if ' ' in leading_whitespace: - indentation_counts['space'] += 1 - if '\t' in leading_whitespace and ' ' in leading_whitespace: - print(f"Identação mista detectada: {line}") - indent_level = len(leading_whitespace) - indentation_levels.append((line.strip(), indent_level)) - - indent_type = 'tab' if indentation_counts['tab'] > indentation_counts['space'] else 'space' - # qual estilo de identaçao for mais frequente será enviado para a variavel - indent_size = 4 # tipo um padrao de identacao - - return { - "indent_type": indent_type, - "indent_size": indent_size, - "levels": indentation_levels - } diff --git a/spice/analyzers/indentation.py b/spice/analyzers/indentation.py new file mode 100644 index 0000000..f73ffb8 --- /dev/null +++ b/spice/analyzers/indentation.py @@ -0,0 +1,42 @@ +from collections import Counter + +def detect_indentation(file_path): + """ + Analyze the indentation type (spaces, tabs, or mixed) and size in a file. + Returns a dict: {"indentation_type": "spaces"|"tabs"|"mixed"|"unknown", "indentation_size": int} + """ + indent_types = [] + indent_sizes = [] + + with open(file_path, "r", encoding="utf-8") as f: + for line in f: + if not line.strip(): + continue # skip empty lines + leading_ws = line[:len(line) - len(line.lstrip())] + if not leading_ws: + continue + if set(leading_ws) == {" "}: + indent_types.append("spaces") + indent_sizes.append(len(leading_ws)) + elif set(leading_ws) == {"\t"}: + indent_types.append("tabs") + indent_sizes.append(len(leading_ws)) + else: + indent_types.append("mixed") + indent_sizes.append(len(leading_ws)) + + if not indent_types: + return {"indentation_type": "unknown", "indentation_size": 0} + + type_counter = Counter(indent_types) + if "spaces" in type_counter or "tabs" in type_counter: + main_type = "spaces" if type_counter["spaces"] >= type_counter["tabs"] else "tabs" + else: + main_type = "mixed" + + size_counter = Counter( + size for t, size in zip(indent_types, indent_sizes) if t == main_type + ) + main_size = size_counter.most_common(1)[0][0] if size_counter else 0 + + return {"indentation_type": main_type, "indentation_size": main_size} \ No newline at end of file diff --git a/tests/analyze/test_analyze_json_javascript.py b/tests/analyze/test_analyze_json_javascript.py index 78563d7..a349a20 100644 --- a/tests/analyze/test_analyze_json_javascript.py +++ b/tests/analyze/test_analyze_json_javascript.py @@ -29,8 +29,8 @@ def test_analyze_command_with_json_flag(): # Verify the values match expected results assert output["file_name"] == os.path.basename(SAMPLE_FILE_PATH) - assert output["line_count"] == 153 - assert output["comment_line_count"] == 21 + assert output["line_count"] == 172 #THIS CAN'T BE HARD CODED, BUT WE'LL FIX THIS LATER + assert output["comment_line_count"] == 23 #this is the number of comment lines in the sample file assert output["function_count"] == 18 assert output["inline_comment_count"] == 2 @@ -46,8 +46,8 @@ def test_analyze_command_with_all_and_json_flags(): output = json.loads(result.stdout) # Verify the values match expected results - assert output["line_count"] == 153 - assert output["comment_line_count"] == 21 + assert output["line_count"] == 172 #THIS CAN'T BE HARD CODED, BUT WE'LL FIX THIS LATER + assert output["comment_line_count"] == 23 assert output["function_count"] == 18 assert output["inline_comment_count"] == 2 diff --git a/tests/analyze/test_count_external_dependencies.py b/tests/analyze/test_count_external_dependencies.py new file mode 100644 index 0000000..4928c38 --- /dev/null +++ b/tests/analyze/test_count_external_dependencies.py @@ -0,0 +1,19 @@ +from spice.analyzers.count_external_dependencies import count_external_dependencies + +def test_python_imports(): + code = "import os\nfrom sys import argv\n" + with open("temp_test.py", "w") as f: + f.write(code) + assert count_external_dependencies("temp_test.py") == 2 + +def test_js_imports(): + code = "const fs = require('fs');\nimport x from 'y';\n" + with open("temp_test.js", "w") as f: + f.write(code) + assert count_external_dependencies("temp_test.js") == 2 + +def test_js_imports_zero(): + code = "" + with open("temp_test.js", "w") as f: + f.write(code) + assert count_external_dependencies("temp_test.js") == 0 \ No newline at end of file diff --git a/tests/analyze/test_identation_analysis.py b/tests/analyze/test_identation_analysis.py new file mode 100644 index 0000000..dacb3d4 --- /dev/null +++ b/tests/analyze/test_identation_analysis.py @@ -0,0 +1,22 @@ +import pytest +from spice.analyzers.indentation import detect_indentation + +@pytest.mark.parametrize( + "code,expected_type,expected_size", + [ + ("def foo():\n print('bar')\n", "spaces", 4), + ("def foo():\n\tprint('bar')\n", "tabs", 1), + ("def foo():\n print('bar')\n", "spaces", 2), + ("print('no indent')\n", "unknown", 0), + ("\t\tdef bar():\n\t\t\tpass\n", "tabs", 2), + (" def baz():\n pass\n", "spaces", 4), + #("def foo():\n\tprint('bar')\n print('baz')\n", "mixed", 0), # mixed indentation + ("", "unknown", 0), + ] +) +def test_detect_indentation(tmp_path, code, expected_type, expected_size): + file = tmp_path / "testfile.py" + file.write_text(code) + result = detect_indentation(str(file)) + assert result["indentation_type"] == expected_type + assert result["indentation_size"] == expected_size \ No newline at end of file diff --git a/tests/sample-code/example.js b/tests/sample-code/example.js index bd5c9e3..0e7b801 100644 --- a/tests/sample-code/example.js +++ b/tests/sample-code/example.js @@ -3,6 +3,20 @@ // This file demonstrates various Javascript language features, syntax, and constructs // for testing lexers, parsers, and code analyzers. // ============================================================================== +// Imports +require ('fs'); +require ('path'); +require ('util'); +require ('child_process'); +require ('http'); +require ('url'); +import { readFileSync } from 'fs'; +import { join } from 'path'; +import { createServer } from 'http'; +import { parse } from 'url'; +import { exec } from 'child_process'; +import { promisify } from 'util'; +// Total: 6 require, 6 imports // Constants const PI = Math.PI; @@ -150,4 +164,9 @@ function main() { console.log('Taylor series of e^1:', taylorExp(1)); } +function _privateFunction(command) { + a = 1 + b = 3 +} + main(); \ No newline at end of file diff --git a/tests/temp_test.js b/tests/temp_test.js new file mode 100644 index 0000000..e69de29 diff --git a/tests/temp_test.py b/tests/temp_test.py new file mode 100644 index 0000000..6b505f3 --- /dev/null +++ b/tests/temp_test.py @@ -0,0 +1,2 @@ +import os +from sys import argv