From 994effe6cf1876016935b87396cb8427dfd13e04 Mon Sep 17 00:00:00 2001
From: CodyKoInABox
Date: Mon, 30 Jun 2025 12:14:18 -0300
Subject: [PATCH 01/17] duplicate code detector
---
spice/analyzers/duplicate_code_detection.py | 108 ++++++++++++++++++++
1 file changed, 108 insertions(+)
create mode 100644 spice/analyzers/duplicate_code_detection.py
diff --git a/spice/analyzers/duplicate_code_detection.py b/spice/analyzers/duplicate_code_detection.py
new file mode 100644
index 0000000..d35b9c3
--- /dev/null
+++ b/spice/analyzers/duplicate_code_detection.py
@@ -0,0 +1,108 @@
+# spice/analyzers/duplicate_code_detection.py
+import hashlib
+import re
+from collections import defaultdict
+
+def detect_duplicate_code(file_path, min_lines=3):
+ """Detect duplicate code blocks in a file.
+
+ Args:
+ file_path (str): Path to the file to analyze
+ min_lines (int): Minimum number of lines to consider as a block
+
+ Returns:
+ dict: Contains duplicate_blocks_count, total_duplicate_lines, and duplicate_percentage
+ """
+ with open(file_path, 'r', encoding='utf-8') as f:
+ code = f.read()
+
+ lines = code.split('\n')
+
+ # Normalize lines by removing comments and extra whitespace
+ normalized_lines = []
+ for line in lines:
+ normalized = _normalize_line(line)
+ normalized_lines.append(normalized)
+
+ # Generate all possible blocks of min_lines or more
+ block_hashes = defaultdict(list)
+ total_lines = len(normalized_lines)
+
+ # Check blocks of different sizes
+ for block_size in range(min_lines, min(total_lines + 1, 20)): # Limit to reasonable block sizes
+ for i in range(total_lines - block_size + 1):
+ block = normalized_lines[i:i + block_size]
+ # Skip blocks that are mostly empty
+ if sum(1 for line in block if line.strip()) < block_size // 2:
+ continue
+
+ block_text = '\n'.join(block)
+ block_hash = hashlib.md5(block_text.encode()).hexdigest()
+ block_hashes[block_hash].append({
+ 'start_line': i + 1,
+ 'end_line': i + block_size,
+ 'size': block_size,
+ 'content': block_text
+ })
+
+ # Find duplicates (blocks that appear more than once)
+ duplicates = {}
+ total_duplicate_lines = 0
+ processed_lines = set()
+
+ for block_hash, occurrences in block_hashes.items():
+ if len(occurrences) > 1:
+ # Choose the largest block size for this hash
+ largest_block = max(occurrences, key=lambda x: x['size'])
+ duplicates[block_hash] = {
+ 'occurrences': len(occurrences),
+ 'locations': occurrences,
+ 'size': largest_block['size']
+ }
+
+ # Count unique duplicate lines (avoid double counting overlapping blocks)
+ for occurrence in occurrences:
+ for line_num in range(occurrence['start_line'], occurrence['end_line'] + 1):
+ if line_num not in processed_lines:
+ processed_lines.add(line_num)
+ total_duplicate_lines += 1
+
+ duplicate_percentage = (total_duplicate_lines / max(total_lines, 1)) * 100
+
+ return {
+ 'duplicate_blocks_count': len(duplicates),
+ 'total_duplicate_lines': total_duplicate_lines,
+ 'duplicate_percentage': round(duplicate_percentage, 2),
+ 'details': duplicates
+ }
+
+def _normalize_line(line):
+ """Normalize a line of code for comparison by removing comments and standardizing whitespace."""
+ # Remove comments (simplified approach)
+ line = re.sub(r'//.*$', '', line) # JS/Go style comments
+ line = re.sub(r'#.*$', '', line) # Python/Ruby style comments
+
+ # Remove string literals (simplified)
+ line = re.sub(r'"[^"]*"', '""', line)
+ line = re.sub(r"'[^']*'", "''", line)
+
+ # Normalize whitespace
+ line = re.sub(r'\s+', ' ', line.strip())
+
+ return line
+
+def get_duplicate_code_summary(file_path):
+ """Get a summary of duplicate code detection for integration with main analyzer.
+
+ Args:
+ file_path (str): Path to the file to analyze
+
+ Returns:
+ dict: Summary of duplicate code analysis
+ """
+ result = detect_duplicate_code(file_path)
+ return {
+ 'duplicate_blocks': result['duplicate_blocks_count'],
+ 'duplicate_lines': result['total_duplicate_lines'],
+ 'duplicate_percentage': result['duplicate_percentage']
+ }
\ No newline at end of file
From eea2014fac1326d6ac0f63b5afb0b9363b2869f7 Mon Sep 17 00:00:00 2001
From: CodyKoInABox
Date: Mon, 30 Jun 2025 12:14:25 -0300
Subject: [PATCH 02/17] avg func size analyzer
---
spice/analyzers/average_function_size.py | 162 +++++++++++++++++++++++
1 file changed, 162 insertions(+)
create mode 100644 spice/analyzers/average_function_size.py
diff --git a/spice/analyzers/average_function_size.py b/spice/analyzers/average_function_size.py
new file mode 100644
index 0000000..8543d8e
--- /dev/null
+++ b/spice/analyzers/average_function_size.py
@@ -0,0 +1,162 @@
+# spice/analyzers/average_function_size.py
+import os
+import re
+from utils.get_lexer import get_lexer_for_file
+
+def calculate_average_function_size(file_path):
+ """Calculate the average size (in lines) of functions in a file.
+
+ Args:
+ file_path (str): Path to the file to analyze
+
+ Returns:
+ float: Average number of lines per function, or 0 if no functions found
+ """
+ with open(file_path, 'r', encoding='utf-8') as f:
+ code = f.read()
+
+ _, ext = os.path.splitext(file_path)
+
+ if ext == '.py':
+ return _analyze_python_functions(code)
+ elif ext == '.js':
+ return _analyze_javascript_functions(code)
+ elif ext == '.rb':
+ return _analyze_ruby_functions(code)
+ elif ext == '.go':
+ return _analyze_go_functions(code)
+ else:
+ return 0.0
+
+def _analyze_python_functions(code):
+ """Analyze Python functions and calculate average size."""
+ lines = code.split('\n')
+ functions = []
+
+ for i, line in enumerate(lines):
+ stripped = line.strip()
+ # Find function definitions
+ if stripped.startswith('def ') and '(' in line:
+ start_line = i
+ # Find the indentation level of the function
+ func_indent = len(line) - len(line.lstrip())
+
+ # Find the end of the function
+ end_line = len(lines)
+ for j in range(i + 1, len(lines)):
+ current_line = lines[j]
+ if current_line.strip() == '':
+ continue
+ current_indent = len(current_line) - len(current_line.lstrip())
+ # Function ends when we find a line with same or less indentation
+ if current_indent <= func_indent and current_line.strip():
+ end_line = j
+ break
+
+ function_size = end_line - start_line
+ functions.append(function_size)
+
+ return sum(functions) / len(functions) if functions else 0.0
+
+def _analyze_javascript_functions(code):
+ """Analyze JavaScript functions and calculate average size."""
+ lines = code.split('\n')
+ functions = []
+
+ i = 0
+ while i < len(lines):
+ line = lines[i].strip()
+
+ # Traditional function declarations
+ if re.match(r'function\s+\w+\s*\(', line) or re.match(r'function\s*\(', line):
+ start_line = i
+ brace_count = line.count('{') - line.count('}')
+
+ # Find the closing brace
+ j = i + 1
+ while j < len(lines) and brace_count > 0:
+ brace_count += lines[j].count('{') - lines[j].count('}')
+ j += 1
+
+ function_size = j - start_line
+ functions.append(function_size)
+ i = j
+ # Arrow functions
+ elif '=>' in line:
+ start_line = i
+ if '{' in line:
+ brace_count = line.count('{') - line.count('}')
+ j = i + 1
+ while j < len(lines) and brace_count > 0:
+ brace_count += lines[j].count('{') - lines[j].count('}')
+ j += 1
+ function_size = j - start_line
+ else:
+ function_size = 1 # Single line arrow function
+ functions.append(function_size)
+ i += 1
+ else:
+ i += 1
+
+ return sum(functions) / len(functions) if functions else 0.0
+
+def _analyze_ruby_functions(code):
+ """Analyze Ruby functions and calculate average size."""
+ lines = code.split('\n')
+ functions = []
+
+ i = 0
+ while i < len(lines):
+ line = lines[i].strip()
+
+ # Method definitions
+ if re.match(r'def\s+\w+', line):
+ start_line = i
+ # Find the corresponding 'end'
+ end_keywords = 1
+ j = i + 1
+
+ while j < len(lines) and end_keywords > 0:
+ current_line = lines[j].strip()
+ # Count def, class, module, if, while, etc. that need 'end'
+ if re.match(r'(def|class|module|if|unless|while|until|for|begin|case)\s', current_line):
+ end_keywords += 1
+ elif current_line == 'end':
+ end_keywords -= 1
+ j += 1
+
+ function_size = j - start_line
+ functions.append(function_size)
+ i = j
+ else:
+ i += 1
+
+ return sum(functions) / len(functions) if functions else 0.0
+
+def _analyze_go_functions(code):
+ """Analyze Go functions and calculate average size."""
+ lines = code.split('\n')
+ functions = []
+
+ i = 0
+ while i < len(lines):
+ line = lines[i].strip()
+
+ # Function declarations
+ if re.match(r'func\s+(\w+\s*)?\(', line) or re.match(r'func\s*\([^)]*\)\s*\w+\s*\(', line):
+ start_line = i
+ brace_count = line.count('{') - line.count('}')
+
+ # Find the closing brace
+ j = i + 1
+ while j < len(lines) and brace_count > 0:
+ brace_count += lines[j].count('{') - lines[j].count('}')
+ j += 1
+
+ function_size = j - start_line
+ functions.append(function_size)
+ i = j
+ else:
+ i += 1
+
+ return sum(functions) / len(functions) if functions else 0.0
\ No newline at end of file
From 7b8c6e5810047e4aef390d319f8f0981f9551128 Mon Sep 17 00:00:00 2001
From: CodyKoInABox
Date: Mon, 30 Jun 2025 12:14:33 -0300
Subject: [PATCH 03/17] asymptototici analyzer
---
spice/analyzers/asymptotic_complexity.py | 306 +++++++++++++++++++++++
1 file changed, 306 insertions(+)
create mode 100644 spice/analyzers/asymptotic_complexity.py
diff --git a/spice/analyzers/asymptotic_complexity.py b/spice/analyzers/asymptotic_complexity.py
new file mode 100644
index 0000000..26a7638
--- /dev/null
+++ b/spice/analyzers/asymptotic_complexity.py
@@ -0,0 +1,306 @@
+# spice/analyzers/asymptotic_complexity.py
+import os
+import re
+from collections import defaultdict
+
+def analyze_asymptotic_complexity(file_path):
+ """Analyze the asymptotic complexity of functions in a file.
+
+ Args:
+ file_path (str): Path to the file to analyze
+
+ Returns:
+ dict: Contains complexity analysis results
+ """
+ with open(file_path, 'r', encoding='utf-8') as f:
+ code = f.read()
+
+ _, ext = os.path.splitext(file_path)
+
+ if ext == '.py':
+ return _analyze_python_complexity(code)
+ elif ext == '.js':
+ return _analyze_javascript_complexity(code)
+ elif ext == '.rb':
+ return _analyze_ruby_complexity(code)
+ elif ext == '.go':
+ return _analyze_go_complexity(code)
+ else:
+ return {'average_complexity': 'O(1)', 'complexity_distribution': {}}
+
+def _analyze_python_complexity(code):
+ """Analyze complexity of Python functions."""
+ lines = code.split('\n')
+ functions = []
+
+ i = 0
+ while i < len(lines):
+ line = lines[i].strip()
+ if line.startswith('def ') and '(' in line:
+ func_name = re.search(r'def\s+(\w+)', line).group(1)
+ start_line = i
+ func_indent = len(lines[i]) - len(lines[i].lstrip())
+
+ # Find function end
+ end_line = len(lines)
+ for j in range(i + 1, len(lines)):
+ if lines[j].strip() == '':
+ continue
+ current_indent = len(lines[j]) - len(lines[j].lstrip())
+ if current_indent <= func_indent and lines[j].strip():
+ end_line = j
+ break
+
+ func_code = '\n'.join(lines[start_line:end_line])
+ complexity = _calculate_complexity(func_code)
+ functions.append({
+ 'name': func_name,
+ 'complexity': complexity,
+ 'start_line': start_line + 1,
+ 'end_line': end_line
+ })
+ i = end_line
+ else:
+ i += 1
+
+ return _summarize_complexity(functions)
+
+def _analyze_javascript_complexity(code):
+ """Analyze complexity of JavaScript functions."""
+ lines = code.split('\n')
+ functions = []
+
+ i = 0
+ while i < len(lines):
+ line = lines[i].strip()
+
+ # Traditional functions
+ func_match = re.search(r'function\s+(\w+)\s*\(', line)
+ if func_match:
+ func_name = func_match.group(1)
+ start_line = i
+ brace_count = line.count('{') - line.count('}')
+
+ j = i + 1
+ while j < len(lines) and brace_count > 0:
+ brace_count += lines[j].count('{') - lines[j].count('}')
+ j += 1
+
+ func_code = '\n'.join(lines[start_line:j])
+ complexity = _calculate_complexity(func_code)
+ functions.append({
+ 'name': func_name,
+ 'complexity': complexity,
+ 'start_line': start_line + 1,
+ 'end_line': j
+ })
+ i = j
+ # Arrow functions
+ elif '=>' in line:
+ arrow_match = re.search(r'(\w+)\s*=.*=>', line)
+ if arrow_match:
+ func_name = arrow_match.group(1)
+ start_line = i
+
+ if '{' in line:
+ brace_count = line.count('{') - line.count('}')
+ j = i + 1
+ while j < len(lines) and brace_count > 0:
+ brace_count += lines[j].count('{') - lines[j].count('}')
+ j += 1
+ end_line = j
+ else:
+ end_line = i + 1
+
+ func_code = '\n'.join(lines[start_line:end_line])
+ complexity = _calculate_complexity(func_code)
+ functions.append({
+ 'name': func_name,
+ 'complexity': complexity,
+ 'start_line': start_line + 1,
+ 'end_line': end_line
+ })
+ i += 1
+ else:
+ i += 1
+
+ return _summarize_complexity(functions)
+
+def _analyze_ruby_complexity(code):
+ """Analyze complexity of Ruby functions."""
+ lines = code.split('\n')
+ functions = []
+
+ i = 0
+ while i < len(lines):
+ line = lines[i].strip()
+ func_match = re.search(r'def\s+(\w+)', line)
+
+ if func_match:
+ func_name = func_match.group(1)
+ start_line = i
+ end_keywords = 1
+ j = i + 1
+
+ while j < len(lines) and end_keywords > 0:
+ current_line = lines[j].strip()
+ if re.match(r'(def|class|module|if|unless|while|until|for|begin|case)\s', current_line):
+ end_keywords += 1
+ elif current_line == 'end':
+ end_keywords -= 1
+ j += 1
+
+ func_code = '\n'.join(lines[start_line:j])
+ complexity = _calculate_complexity(func_code)
+ functions.append({
+ 'name': func_name,
+ 'complexity': complexity,
+ 'start_line': start_line + 1,
+ 'end_line': j
+ })
+ i = j
+ else:
+ i += 1
+
+ return _summarize_complexity(functions)
+
+def _analyze_go_complexity(code):
+ """Analyze complexity of Go functions."""
+ lines = code.split('\n')
+ functions = []
+
+ i = 0
+ while i < len(lines):
+ line = lines[i].strip()
+ func_match = re.search(r'func\s+(\w+)\s*\(', line)
+
+ if func_match:
+ func_name = func_match.group(1)
+ start_line = i
+ brace_count = line.count('{') - line.count('}')
+
+ j = i + 1
+ while j < len(lines) and brace_count > 0:
+ brace_count += lines[j].count('{') - lines[j].count('}')
+ j += 1
+
+ func_code = '\n'.join(lines[start_line:j])
+ complexity = _calculate_complexity(func_code)
+ functions.append({
+ 'name': func_name,
+ 'complexity': complexity,
+ 'start_line': start_line + 1,
+ 'end_line': j
+ })
+ i = j
+ else:
+ i += 1
+
+ return _summarize_complexity(functions)
+
+def _calculate_complexity(code):
+ """Calculate the asymptotic complexity of a code block."""
+ complexity_score = 1 # Base complexity O(1)
+
+ # Count nested loops and conditionals
+ loop_patterns = [
+ r'\bfor\b', # for loops
+ r'\bwhile\b', # while loops
+ r'\bforeach\b', # foreach (Ruby)
+ r'\.each\b', # .each (Ruby)
+ r'\.map\b', # .map
+ r'\.filter\b', # .filter
+ r'\.reduce\b' # .reduce
+ ]
+
+ conditional_patterns = [
+ r'\bif\b',
+ r'\belse\b',
+ r'\belif\b',
+ r'\bunless\b', # Ruby
+ r'\bcase\b',
+ r'\bswitch\b'
+ ]
+
+ recursive_patterns = [
+ r'\breturn\s+\w+\(', # potential recursion
+ ]
+
+ # Count nesting levels
+ nesting_level = 0
+ max_nesting = 0
+ lines = code.split('\n')
+
+ for line in lines:
+ stripped = line.strip()
+
+ # Check for loop patterns
+ for pattern in loop_patterns:
+ if re.search(pattern, stripped):
+ complexity_score *= 2 # Each loop adds a factor
+ nesting_level += 1
+ break
+
+ # Check for conditional patterns (less impact than loops)
+ for pattern in conditional_patterns:
+ if re.search(pattern, stripped):
+ complexity_score += 1
+ break
+
+ # Check for recursion
+ for pattern in recursive_patterns:
+ if re.search(pattern, stripped):
+ complexity_score *= 3 # Recursion significantly increases complexity
+ break
+
+ # Track nesting (simplified)
+ if any(char in stripped for char in ['{', 'do']):
+ nesting_level += 1
+ if any(char in stripped for char in ['}', 'end']):
+ nesting_level = max(0, nesting_level - 1)
+
+ max_nesting = max(max_nesting, nesting_level)
+
+ # Apply nesting multiplier
+ if max_nesting > 2:
+ complexity_score *= max_nesting
+
+ # Map score to Big O notation
+ if complexity_score <= 1:
+ return 'O(1)'
+ elif complexity_score <= 3:
+ return 'O(log n)'
+ elif complexity_score <= 10:
+ return 'O(n)'
+ elif complexity_score <= 25:
+ return 'O(n log n)'
+ elif complexity_score <= 50:
+ return 'O(n²)'
+ elif complexity_score <= 100:
+ return 'O(n³)'
+ else:
+ return 'O(2^n)'
+
+def _summarize_complexity(functions):
+ """Summarize complexity analysis results."""
+ if not functions:
+ return {
+ 'average_complexity': 'O(1)',
+ 'complexity_distribution': {},
+ 'total_functions': 0
+ }
+
+ # Count complexity distribution
+ complexity_counts = defaultdict(int)
+ for func in functions:
+ complexity_counts[func['complexity']] += 1
+
+ # Calculate "average" complexity (most common)
+ most_common_complexity = max(complexity_counts.items(), key=lambda x: x[1])[0]
+
+ return {
+ 'average_complexity': most_common_complexity,
+ 'complexity_distribution': dict(complexity_counts),
+ 'total_functions': len(functions),
+ 'function_details': functions
+ }
\ No newline at end of file
From 65f5e5196a094c9b9e0fed30fddc952371223d96 Mon Sep 17 00:00:00 2001
From: CodyKoInABox
Date: Mon, 30 Jun 2025 12:14:37 -0300
Subject: [PATCH 04/17] add new analyzers
---
spice/analyze.py | 39 +++++++++++++++++++++++++++++++++++----
1 file changed, 35 insertions(+), 4 deletions(-)
diff --git a/spice/analyze.py b/spice/analyze.py
index ed8fd54..0b58d23 100644
--- a/spice/analyze.py
+++ b/spice/analyze.py
@@ -11,7 +11,9 @@ def analyze_file(file_path: str, selected_stats: Optional[List[str]] = None) ->
file_path (str): Path to the file to analyze
selected_stats (list, optional): List of stats to compute. If None, compute all stats.
Valid stats are: "line_count", "function_count", "comment_line_count",
- "inline_comment_count", "indentation_level"
+ "inline_comment_count", "indentation_level", "external_dependencies_count",
+ "method_type_count", "comment_ratio", "average_function_size",
+ "duplicate_code_detection", "asymptotic_complexity"
Returns:
dict: Dictionary containing the requested stats and file information
@@ -34,8 +36,13 @@ def analyze_file(file_path: str, selected_stats: Optional[List[str]] = None) ->
if not ext:
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", "comment_ratio"]
+ # Define valid stats (including new ones)
+ valid_stats = [
+ "line_count", "function_count", "comment_line_count", "inline_comment_count",
+ "indentation_level", "external_dependencies_count", "method_type_count",
+ "comment_ratio", "average_function_size", "duplicate_code_detection",
+ "asymptotic_complexity"
+ ]
# default to all stats if none specified
if selected_stats is None:
@@ -110,8 +117,32 @@ def analyze_file(file_path: str, selected_stats: Optional[List[str]] = None) ->
if "comment_ratio" in selected_stats:
from spice.analyzers.count_comment_ratio import count_comment_ratio
results["comment_ratio"] = count_comment_ratio(file_path)
+
+ # NEW FEATURES BELOW
+
+ # average function size if requested
+ if "average_function_size" in selected_stats:
+ from spice.analyzers.average_function_size import calculate_average_function_size
+ results["average_function_size"] = calculate_average_function_size(file_path)
+
+ # duplicate code detection if requested
+ if "duplicate_code_detection" in selected_stats:
+ from spice.analyzers.duplicate_code_detection import get_duplicate_code_summary
+ duplicate_info = get_duplicate_code_summary(file_path)
+ results["duplicate_blocks"] = duplicate_info["duplicate_blocks"]
+ results["duplicate_lines"] = duplicate_info["duplicate_lines"]
+ results["duplicate_percentage"] = duplicate_info["duplicate_percentage"]
+
+ # asymptotic complexity analysis if requested
+ if "asymptotic_complexity" in selected_stats:
+ from spice.analyzers.asymptotic_complexity import analyze_asymptotic_complexity
+ complexity_info = analyze_asymptotic_complexity(file_path)
+ results["average_complexity"] = complexity_info["average_complexity"]
+ results["complexity_distribution"] = complexity_info["complexity_distribution"]
+ results["total_analyzed_functions"] = complexity_info.get("total_functions", 0)
+
return results
except Exception as e:
# Add context to any errors that occur during analysis
- raise Exception(f"Error analyzing file {file_path}: {str(e)}")
+ raise Exception(f"Error analyzing file {file_path}: {str(e)}")
\ No newline at end of file
From 8571da721d333b3a8fd72ac557d54060ff165994 Mon Sep 17 00:00:00 2001
From: CodyKoInABox
Date: Mon, 30 Jun 2025 12:14:44 -0300
Subject: [PATCH 05/17] add new analyzers to menu
---
cli/commands/analyze.py | 28 +++++++++++++++++++++++++---
1 file changed, 25 insertions(+), 3 deletions(-)
diff --git a/cli/commands/analyze.py b/cli/commands/analyze.py
index 7d4dda9..a13362c 100644
--- a/cli/commands/analyze.py
+++ b/cli/commands/analyze.py
@@ -63,7 +63,7 @@ def analyze_command(file, all, json_output, LANG_FILE):
# load translations
messages = get_translation(LANG_FILE)
- # define available stats
+ # define available stats (updated with new features)
available_stats = [
"line_count",
"function_count",
@@ -73,9 +73,12 @@ def analyze_command(file, all, json_output, LANG_FILE):
"external_dependencies_count",
"method_type_count",
"comment_ratio",
+ "average_function_size",
+ "duplicate_code_detection",
+ "asymptotic_complexity"
]
- # dictionary for the stats
+ # dictionary for the stats (updated with new features)
stats_labels = {
"line_count": messages.get("line_count_option", "Line Count"),
"function_count": messages.get("function_count_option", "Function Count"),
@@ -87,6 +90,9 @@ def analyze_command(file, all, json_output, LANG_FILE):
"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"),
+ "average_function_size": messages.get("average_function_size_option", "Average Function Size"),
+ "duplicate_code_detection": messages.get("duplicate_code_detection_option", "Duplicate Code Detection"),
+ "asymptotic_complexity": messages.get("asymptotic_complexity_option", "Asymptotic Complexity Analysis")
}
# If --all flag is used, skip the selection menu and use all stats
@@ -156,10 +162,26 @@ def analyze_command(file, all, json_output, LANG_FILE):
print(f"{messages.get('private_methods_count_option', 'Private Methods Count')}: {mtc['private']}")
continue
- if stat == "indentation_level" and "indentation_type" in results:
+ elif stat == "indentation_level" and "indentation_type" in results:
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 == "duplicate_code_detection" and any(key in results for key in ["duplicate_blocks", "duplicate_lines", "duplicate_percentage"]):
+ print(f"{messages.get('duplicate_blocks', 'Duplicate Blocks')}: {results.get('duplicate_blocks', 'N/A')}")
+ print(f"{messages.get('duplicate_lines', 'Duplicate Lines')}: {results.get('duplicate_lines', 'N/A')}")
+ print(f"{messages.get('duplicate_percentage', 'Duplicate Percentage')}: {results.get('duplicate_percentage', 'N/A')}%")
+ continue
+
+ elif stat == "asymptotic_complexity" and any(key in results for key in ["average_complexity", "complexity_distribution", "total_analyzed_functions"]):
+ print(f"{messages.get('average_complexity', 'Average Complexity')}: {results.get('average_complexity', 'N/A')}")
+ print(f"{messages.get('total_analyzed_functions', 'Total Analyzed Functions')}: {results.get('total_analyzed_functions', 'N/A')}")
+ complexity_dist = results.get('complexity_distribution', {})
+ if complexity_dist:
+ print(f"{messages.get('complexity_distribution', 'Complexity Distribution')}:")
+ for complexity, count in complexity_dist.items():
+ print(f" {complexity}: {count}")
+ continue
elif stat in results:
print(f"{stats_labels[stat]}: {results[stat]}")
From b4f76314151216050603fbaf5831efcfd277f4fb Mon Sep 17 00:00:00 2001
From: CodyKoInABox
Date: Mon, 30 Jun 2025 12:14:49 -0300
Subject: [PATCH 06/17] translations for new analyzers
---
cli/translations/en.py | 10 ++++++++++
1 file changed, 10 insertions(+)
diff --git a/cli/translations/en.py b/cli/translations/en.py
index 3b2c3d7..c0e25ea 100644
--- a/cli/translations/en.py
+++ b/cli/translations/en.py
@@ -31,4 +31,14 @@
"private_methods_count_option": "Private Methods Count",
"public_methods_count_option": "Public Methods Count",
"comment_ratio_option": "Comment to Code Ratio",
+ # new
+ "average_function_size_option": "Average Function Size",
+ "duplicate_code_detection_option": "Duplicate Code Detection",
+ "asymptotic_complexity_option": "Asymptotic Complexity Analysis",
+ "duplicate_blocks": "Duplicate Blocks",
+ "duplicate_lines": "Duplicate Lines",
+ "duplicate_percentage": "Duplicate Percentage",
+ "average_complexity": "Average Complexity",
+ "total_analyzed_functions": "Total Analyzed Functions",
+ "complexity_distribution": "Complexity Distribution"
}
\ No newline at end of file
From fb185d8073e33f0730b7dec057274f5af4d6ce07 Mon Sep 17 00:00:00 2001
From: CodyKoInABox
Date: Mon, 30 Jun 2025 12:34:36 -0300
Subject: [PATCH 07/17] code complexity componeent
---
.../pages/components/ComplexityCard.tsx | 37 +++++++++++++++++++
1 file changed, 37 insertions(+)
create mode 100644 spicecloud/pages/components/ComplexityCard.tsx
diff --git a/spicecloud/pages/components/ComplexityCard.tsx b/spicecloud/pages/components/ComplexityCard.tsx
new file mode 100644
index 0000000..c45f5ad
--- /dev/null
+++ b/spicecloud/pages/components/ComplexityCard.tsx
@@ -0,0 +1,37 @@
+import React from 'react';
+import { styles } from '../utils/styles';
+
+interface Props {
+ averageComplexity?: string;
+ distribution?: Record;
+ totalFunctions?: number;
+}
+
+export const ComplexityCard: React.FC = ({
+ averageComplexity = 'N/A',
+ distribution,
+ totalFunctions,
+}) => {
+ return (
+
+
+
Complexity
+ {averageComplexity}
+
+
+
+ {totalFunctions !== undefined && (
+
+ Functions analysed: {totalFunctions}
+
+ )}
+ {distribution &&
+ Object.entries(distribution).map(([bigO, qty]) => (
+
+ {bigO}: {qty}
+
+ ))}
+
+
+ );
+};
From 25b75282e3d726e5083f5f545d9918100aaf5eba Mon Sep 17 00:00:00 2001
From: CodyKoInABox
Date: Mon, 30 Jun 2025 12:34:43 -0300
Subject: [PATCH 08/17] duplicate code componeent
---
.../pages/components/DuplicateCodeCard.tsx | 32 +++++++++++++++++++
1 file changed, 32 insertions(+)
create mode 100644 spicecloud/pages/components/DuplicateCodeCard.tsx
diff --git a/spicecloud/pages/components/DuplicateCodeCard.tsx b/spicecloud/pages/components/DuplicateCodeCard.tsx
new file mode 100644
index 0000000..a9ded7d
--- /dev/null
+++ b/spicecloud/pages/components/DuplicateCodeCard.tsx
@@ -0,0 +1,32 @@
+import React from 'react';
+import { styles } from '../utils/styles';
+
+interface Props {
+ percentage?: number;
+ blocks?: number;
+ lines?: number;
+}
+
+export const DuplicateCodeCard: React.FC = ({
+ percentage = 0,
+ blocks,
+ lines,
+}) => {
+ return (
+
+
+
Duplicate Code
+ {percentage.toFixed(2)}%
+
+
+
+ {blocks !== undefined && (
+
Duplicate blocks: {blocks}
+ )}
+ {lines !== undefined && (
+
Duplicate lines: {lines}
+ )}
+
+
+ );
+};
From 96e51a8183d91f29614c5e86faeb4a512b7fa0d2 Mon Sep 17 00:00:00 2001
From: CodyKoInABox
Date: Mon, 30 Jun 2025 12:35:16 -0300
Subject: [PATCH 09/17] update metrics grid componeent to include new metrics
---
spicecloud/pages/components/MetricsGrid.tsx | 79 ++++++++++++++-------
1 file changed, 53 insertions(+), 26 deletions(-)
diff --git a/spicecloud/pages/components/MetricsGrid.tsx b/spicecloud/pages/components/MetricsGrid.tsx
index 1cbeda7..f3933dd 100644
--- a/spicecloud/pages/components/MetricsGrid.tsx
+++ b/spicecloud/pages/components/MetricsGrid.tsx
@@ -3,52 +3,79 @@ import { MetricData } from '../utils/types';
import { MetricCard } from './MetricCard';
import { MethodTypeCard } from './MethodTypeCard';
import { IndentationCard } from './IndentationCard';
+import { ComplexityCard } from './ComplexityCard';
+import { DuplicateCodeCard } from './DuplicateCodeCard';
import { styles } from '../utils/styles';
interface MetricsGridProps {
selectedFile: MetricData;
}
+// keys handled by special cards ↓
+const SKIP_KEYS = [
+ 'file_name',
+ 'file_path',
+ 'file_size',
+ 'file_extension',
+ 'indentation_type',
+ 'indentation_size',
+ 'method_type_count',
+ 'duplicate_blocks',
+ 'duplicate_lines',
+ 'duplicate_percentage',
+ 'average_complexity',
+ 'complexity_distribution',
+ 'total_analyzed_functions',
+];
+
export const MetricsGrid: React.FC = ({ selectedFile }) => {
+ const m = selectedFile.metrics;
+
return (
- {Object.entries(selectedFile.metrics).map(([key, value]) => {
- // Skip file info metrics
- if (['file_name', 'file_path', 'file_size', 'file_extension'].includes(key)) {
- return null;
- }
+ {/* generic & existing cards */}
+ {Object.entries(m).map(([key, value]) => {
+ if (SKIP_KEYS.includes(key)) return null;
- // Handle method type count specially
- if (key === 'method_type_count' && typeof value === 'object' && value !== null) {
+ if (key === 'method_type_count') {
return (
-
);
}
- // Skip indentation keys as they'll be handled in their own card
- if (key === 'indentation_type' || key === 'indentation_size') {
- return null;
- }
-
- return (
-
- );
+ return ;
})}
- {/* Indentation Card */}
- {(selectedFile.metrics.indentation_type || selectedFile.metrics.indentation_size) && (
-
+ )}
+
+ {/* duplicate‑code */}
+ {(m.duplicate_percentage !== undefined ||
+ m.duplicate_blocks !== undefined ||
+ m.duplicate_lines !== undefined) && (
+
+ )}
+
+ {/* complexity */}
+ {m.average_complexity && (
+
)}
);
-};
\ No newline at end of file
+};
From fb40eec7030af7772ff9216c0b6c05caf2c5e89e Mon Sep 17 00:00:00 2001
From: CodyKoInABox
Date: Mon, 30 Jun 2025 12:35:23 -0300
Subject: [PATCH 10/17] new styles for new componeents
---
spicecloud/pages/utils/styles.ts | 16 +++++++++++++++-
1 file changed, 15 insertions(+), 1 deletion(-)
diff --git a/spicecloud/pages/utils/styles.ts b/spicecloud/pages/utils/styles.ts
index 5c8c2f0..865a9a8 100644
--- a/spicecloud/pages/utils/styles.ts
+++ b/spicecloud/pages/utils/styles.ts
@@ -181,7 +181,8 @@ export const styles = {
alignItems: 'center',
gap: '0.75rem',
marginBottom: '1rem',
- color: '#000000'
+ color: '#000000',
+ cursor: 'pointer'
},
metricIcon: {
fontSize: '1.5rem',
@@ -198,6 +199,19 @@ export const styles = {
color: '#000000',
marginBottom: '0.5rem'
},
+ metricDetails: {
+ /* corpo que aparece quando o card está aberto */
+ marginTop: '0.5rem',
+ display: 'flex',
+ flexDirection: 'column' as const,
+ gap: '0.25rem'
+ },
+ detailLine: {
+ /* linha individual dentro do corpo */
+ fontSize: '0.875rem',
+ opacity: 0.8, // ligeiro cinza
+ color: '#000000'
+ },
progressBar: {
width: '100%',
height: '0.5rem',
From 025bf1ebb0bcfc5ecdbf5305ed2d365cf85cfe0f Mon Sep 17 00:00:00 2001
From: CodyKoInABox
Date: Mon, 30 Jun 2025 12:35:27 -0300
Subject: [PATCH 11/17] new types for new metrics
---
spicecloud/pages/utils/types.ts | 26 ++++++++++++++++++++++++--
1 file changed, 24 insertions(+), 2 deletions(-)
diff --git a/spicecloud/pages/utils/types.ts b/spicecloud/pages/utils/types.ts
index 86a681d..0808328 100644
--- a/spicecloud/pages/utils/types.ts
+++ b/spicecloud/pages/utils/types.ts
@@ -4,7 +4,29 @@ export interface MetricData {
timestamp: number;
file_name: string;
file_path: string;
- metrics: any;
+ metrics: {
+ file_extension: string;
+ line_count: number;
+ comment_line_count: number;
+ inline_comment_count: number;
+ indentation_type: string;
+ indentation_size: number;
+ function_count: number;
+ external_dependencies_count: number;
+ method_type_count: { private: number; public: number };
+ comment_ratio: string;
+ average_function_size?: number;
+
+ /* duplicate code */
+ duplicate_blocks?: number;
+ duplicate_lines?: number;
+ duplicate_percentage?: number;
+
+ /* complexity */
+ average_complexity?: string;
+ complexity_distribution?: Record;
+ total_analyzed_functions?: number;
+ };
age: number;
readable_timestamp: string;
-}
\ No newline at end of file
+}
From fea2a995249f4aec149fe7acb63f364e61713c21 Mon Sep 17 00:00:00 2001
From: CodyKoInABox
Date: Mon, 30 Jun 2025 12:45:52 -0300
Subject: [PATCH 12/17] concat avg func lines
---
spicecloud/pages/components/MetricCard.tsx | 44 ++++++++++++++--------
1 file changed, 29 insertions(+), 15 deletions(-)
diff --git a/spicecloud/pages/components/MetricCard.tsx b/spicecloud/pages/components/MetricCard.tsx
index 85f7bb4..bf40bed 100644
--- a/spicecloud/pages/components/MetricCard.tsx
+++ b/spicecloud/pages/components/MetricCard.tsx
@@ -1,5 +1,10 @@
import React from 'react';
-import { formatMetricValue, getMetricIcon, getMetricColor, formatLabel } from '../utils/utils';
+import {
+ formatMetricValue,
+ getMetricIcon,
+ getMetricColor,
+ formatLabel,
+} from '../utils/utils';
import { styles } from '../utils/styles';
interface MetricCardProps {
@@ -7,12 +12,22 @@ interface MetricCardProps {
value: any;
}
-export const MetricCard: React.FC = ({ metricKey, value }) => {
+export const MetricCard: React.FC = ({
+ metricKey,
+ value,
+}) => {
+ /* 👉 custom display for average_function_size */
+ const displayValue =
+ metricKey === 'average_function_size'
+ ? `${parseFloat(value).toFixed(1)} lines`
+ : formatMetricValue(metricKey, value);
+
return (
- {
@@ -26,23 +41,22 @@ export const MetricCard: React.FC = ({ metricKey, value }) => {
{getMetricIcon(metricKey)}
-
- {formatLabel(metricKey)}
-
-
-
- {formatMetricValue(metricKey, value)}
+
{formatLabel(metricKey)}
+
+ {displayValue}
+
+ {/* progress bar only for ratio‑type metrics */}
{metricKey.includes('ratio') && (
)}
);
-};
\ No newline at end of file
+};
From 290594e708c8e261d1a29bd3257d498f0948a8de Mon Sep 17 00:00:00 2001
From: CodyKoInABox
Date: Mon, 30 Jun 2025 13:26:46 -0300
Subject: [PATCH 13/17] remove file size metric
---
spicecloud/pages/components/FileHeader.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/spicecloud/pages/components/FileHeader.tsx b/spicecloud/pages/components/FileHeader.tsx
index 352e17b..c86a6ff 100644
--- a/spicecloud/pages/components/FileHeader.tsx
+++ b/spicecloud/pages/components/FileHeader.tsx
@@ -26,7 +26,7 @@ export const FileHeader: React.FC = ({ selectedFile }) => {
🕒 {selectedFile.readable_timestamp}
- 💾 {selectedFile.metrics.file_size ? `${(selectedFile.metrics.file_size / 1024).toFixed(1)} KB` : 'N/A'}
+ {/* 💾 {selectedFile.metrics.file_size ? `${(selectedFile.metrics.file_size / 1024).toFixed(1)} KB` : 'N/A'} */}
📂 {selectedFile.metrics.file_extension || 'N/A'}
From 3f0092c608e54a7dcc2bf8bb7f6123228d822292 Mon Sep 17 00:00:00 2001
From: CodyKoInABox
Date: Mon, 30 Jun 2025 13:38:30 -0300
Subject: [PATCH 14/17] spice cloud logo
---
spicecloud/public/spicecloud-logo.png | Bin 0 -> 7040 bytes
1 file changed, 0 insertions(+), 0 deletions(-)
create mode 100644 spicecloud/public/spicecloud-logo.png
diff --git a/spicecloud/public/spicecloud-logo.png b/spicecloud/public/spicecloud-logo.png
new file mode 100644
index 0000000000000000000000000000000000000000..3a2f34710fddb50bd7d7d8cfb477bc146a84435f
GIT binary patch
literal 7040
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
g4blHE!!7%4T8uUCF_v?1ArTUGynhq
literal 0
HcmV?d00001
From 622eedad616d58fe81d021d7f72a06711598f062 Mon Sep 17 00:00:00 2001
From: CodyKoInABox
Date: Mon, 30 Jun 2025 13:40:51 -0300
Subject: [PATCH 15/17] spicecloud logo with textx
---
spicecloud/public/spicecloud-logo-text.png | Bin 0 -> 15569 bytes
1 file changed, 0 insertions(+), 0 deletions(-)
create mode 100644 spicecloud/public/spicecloud-logo-text.png
diff --git a/spicecloud/public/spicecloud-logo-text.png b/spicecloud/public/spicecloud-logo-text.png
new file mode 100644
index 0000000000000000000000000000000000000000..f03d28c5a21201c1d95c689140bee46e49a563ad
GIT binary patch
literal 15569
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
S4Sc5~1Ej?j#A-#1g8l~tUU~ii
literal 0
HcmV?d00001
From 37a0e881c7859cf106128a00af4ed0761b3c3233 Mon Sep 17 00:00:00 2001
From: CodyKoInABox
Date: Mon, 30 Jun 2025 13:41:18 -0300
Subject: [PATCH 16/17] use spicecloud logo for favicon
---
spicecloud/pages/_document.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/spicecloud/pages/_document.tsx b/spicecloud/pages/_document.tsx
index f0a5df0..0e650c7 100644
--- a/spicecloud/pages/_document.tsx
+++ b/spicecloud/pages/_document.tsx
@@ -4,7 +4,7 @@ export default function Document() {
return (
-
+
From 395a40d10419092557db15cdf34b246bdc5fa8d3 Mon Sep 17 00:00:00 2001
From: CodyKoInABox
Date: Mon, 30 Jun 2025 13:41:45 -0300
Subject: [PATCH 17/17] use spice cloud logo on header
---
spicecloud/pages/components/Header.tsx | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/spicecloud/pages/components/Header.tsx b/spicecloud/pages/components/Header.tsx
index 59d15f9..0e94f14 100644
--- a/spicecloud/pages/components/Header.tsx
+++ b/spicecloud/pages/components/Header.tsx
@@ -14,8 +14,8 @@ export const Header: React.FC = ({ dataLength, loading, onRefresh }
SpiceCloud | Powered by SpiceCodeCLI