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]}") 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 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 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 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 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 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 ( - +
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} +

+ ))} +
+
+ ); +}; 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}

+ )} +
+
+ ); +}; 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'}
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 }

SpiceCode Logo SpiceCloud | Powered by SpiceCodeCLI 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 +}; 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 +}; 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', 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 +} diff --git a/spicecloud/public/spicecloud-logo-text.png b/spicecloud/public/spicecloud-logo-text.png new file mode 100644 index 0000000..f03d28c Binary files /dev/null and b/spicecloud/public/spicecloud-logo-text.png differ diff --git a/spicecloud/public/spicecloud-logo.png b/spicecloud/public/spicecloud-logo.png new file mode 100644 index 0000000..3a2f347 Binary files /dev/null and b/spicecloud/public/spicecloud-logo.png differ