Add performance and secrets detection rules
This commit is contained in:
272
src/rules/performance.py
Normal file
272
src/rules/performance.py
Normal file
@@ -0,0 +1,272 @@
|
||||
"""Performance issue detection rules."""
|
||||
|
||||
import re
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
import tree_sitter
|
||||
|
||||
from src.analyzers.base import (
|
||||
Analyzer,
|
||||
Finding,
|
||||
FindingCategory,
|
||||
SeverityLevel,
|
||||
)
|
||||
|
||||
|
||||
class InefficientLoopAnalyzer(Analyzer):
|
||||
"""Detect inefficient loop patterns."""
|
||||
|
||||
def rule_id(self) -> str:
|
||||
return "performance.inefficient_loop"
|
||||
|
||||
def rule_name(self) -> str:
|
||||
return "Inefficient Loop Detection"
|
||||
|
||||
def severity(self) -> SeverityLevel:
|
||||
return SeverityLevel.MEDIUM
|
||||
|
||||
def category(self) -> FindingCategory:
|
||||
return FindingCategory.PERFORMANCE
|
||||
|
||||
def analyze(
|
||||
self, source_code: str, file_path: Path, tree: tree_sitter.Tree
|
||||
) -> list[Finding]:
|
||||
findings = []
|
||||
loops = self._get_loops(tree.root_node)
|
||||
|
||||
for loop in loops:
|
||||
if self._is_inefficient(loop, source_code):
|
||||
line = self._get_line_number(loop, source_code)
|
||||
findings.append(
|
||||
Finding(
|
||||
rule_id=self.rule_id(),
|
||||
rule_name=self.rule_name(),
|
||||
severity=self.severity(),
|
||||
category=self.category(),
|
||||
message="Inefficient loop pattern detected",
|
||||
suggestion="Consider using list comprehension or built-in functions",
|
||||
file_path=file_path,
|
||||
line_number=line,
|
||||
column=self._get_column(loop),
|
||||
node=loop,
|
||||
)
|
||||
)
|
||||
return findings
|
||||
|
||||
def _get_loops(self, node: tree_sitter.Node) -> list[tree_sitter.Node]:
|
||||
loops = []
|
||||
if hasattr(node, "type") and node.type in {"for_statement", "while_statement", "for_in_statement"}:
|
||||
loops.append(node)
|
||||
if hasattr(node, "children"):
|
||||
for child in node.children:
|
||||
loops.extend(self._get_loops(child))
|
||||
return loops
|
||||
|
||||
def _is_inefficient(self, loop: tree_sitter.Node, source_code: str) -> bool:
|
||||
loop_text = self._get_node_text(loop, source_code)
|
||||
inefficient_patterns = [
|
||||
r"for\s+\w+\s+in\s+range\s*\(\s*len\s*\(",
|
||||
r"while\s+True\s*:.*\s+if.*:\s+break",
|
||||
r"for\s+\w+\s+in\s+.*:\s+\w+\.append",
|
||||
]
|
||||
return any(re.search(p, loop_text) for p in inefficient_patterns)
|
||||
|
||||
def _get_node_text(self, node: tree_sitter.Node, source_code: str) -> str:
|
||||
if hasattr(node, "start_byte") and hasattr(node, "end_byte"):
|
||||
return source_code[node.start_byte:node.end_byte]
|
||||
return ""
|
||||
|
||||
def _get_line_number(self, node: tree_sitter.Node, source_code: str) -> int:
|
||||
lines = source_code.split("\n")
|
||||
start_byte = node.start_byte if hasattr(node, "start_byte") else 0
|
||||
pos = 0
|
||||
for line_num, line_text in enumerate(lines, 1):
|
||||
if pos + len(line_text) >= start_byte:
|
||||
return line_num
|
||||
pos += len(line_text) + 1
|
||||
return 1
|
||||
|
||||
def _get_column(self, node: tree_sitter.Node) -> int:
|
||||
return node.start_column if hasattr(node, "start_column") else 0
|
||||
|
||||
|
||||
class RedundantOperationAnalyzer(Analyzer):
|
||||
"""Detect redundant operations."""
|
||||
|
||||
def rule_id(self) -> str:
|
||||
return "performance.redundant_operation"
|
||||
|
||||
def rule_name(self) -> str:
|
||||
return "Redundant Operation Detection"
|
||||
|
||||
def severity(self) -> SeverityLevel:
|
||||
return SeverityLevel.LOW
|
||||
|
||||
def category(self) -> FindingCategory:
|
||||
return FindingCategory.PERFORMANCE
|
||||
|
||||
def analyze(
|
||||
self, source_code: str, file_path: Path, tree: tree_sitter.Tree
|
||||
) -> list[Finding]:
|
||||
findings = []
|
||||
calls = self._get_calls(tree.root_node)
|
||||
|
||||
for call in calls:
|
||||
func_name = self._get_function_name(call)
|
||||
if func_name in {"list", "str", "dict", "set"}:
|
||||
args = self._get_arguments(call)
|
||||
if self._is_redundant(func_name, args):
|
||||
line = self._get_line_number(call, source_code)
|
||||
findings.append(
|
||||
Finding(
|
||||
rule_id=self.rule_id(),
|
||||
rule_name=self.rule_name(),
|
||||
severity=self.severity(),
|
||||
category=self.category(),
|
||||
message=f"Redundant {func_name}() call detected",
|
||||
suggestion="Remove unnecessary type conversion",
|
||||
file_path=file_path,
|
||||
line_number=line,
|
||||
column=self._get_column(call),
|
||||
node=call,
|
||||
)
|
||||
)
|
||||
return findings
|
||||
|
||||
def _get_calls(self, node: tree_sitter.Node) -> list[tree_sitter.Node]:
|
||||
calls = []
|
||||
if hasattr(node, "type") and node.type == "call":
|
||||
calls.append(node)
|
||||
if hasattr(node, "children"):
|
||||
for child in node.children:
|
||||
calls.extend(self._get_calls(child))
|
||||
return calls
|
||||
|
||||
def _get_function_name(self, call: tree_sitter.Node) -> str:
|
||||
if hasattr(call, "children") and len(call.children) > 0:
|
||||
func = call.children[0]
|
||||
if hasattr(func, "text"):
|
||||
text = func.text
|
||||
return text.decode() if isinstance(text, bytes) else str(text)
|
||||
return ""
|
||||
|
||||
def _get_arguments(self, call: tree_sitter.Node) -> list[str]:
|
||||
args = []
|
||||
if hasattr(call, "children"):
|
||||
for child in call.children:
|
||||
if hasattr(child, "text"):
|
||||
text = child.text
|
||||
args.append(text.decode() if isinstance(text, bytes) else str(text))
|
||||
return args
|
||||
|
||||
def _is_redundant(self, func_name: str, args: list[str]) -> bool:
|
||||
if len(args) == 0:
|
||||
return True
|
||||
arg = args[0].lower()
|
||||
redundant_map = {
|
||||
"list": ["list(", "[", "list("],
|
||||
"str": ["str(", "'", '"', "str("],
|
||||
"dict": ["dict(", "{", "dict("],
|
||||
"set": ["set(", "{", "set("],
|
||||
}
|
||||
return any(arg.startswith(p) for p in redundant_map.get(func_name, []))
|
||||
|
||||
def _get_line_number(self, node: tree_sitter.Node, source_code: str) -> int:
|
||||
lines = source_code.split("\n")
|
||||
start_byte = node.start_byte if hasattr(node, "start_byte") else 0
|
||||
pos = 0
|
||||
for line_num, line_text in enumerate(lines, 1):
|
||||
if pos + len(line_text) >= start_byte:
|
||||
return line_num
|
||||
pos += len(line_text) + 1
|
||||
return 1
|
||||
|
||||
def _get_column(self, node: tree_sitter.Node) -> int:
|
||||
return node.start_column if hasattr(node, "start_column") else 0
|
||||
|
||||
|
||||
class UnnecessaryCopyAnalyzer(Analyzer):
|
||||
"""Detect unnecessary list copies."""
|
||||
|
||||
def rule_id(self) -> str:
|
||||
return "performance.unnecessary_copy"
|
||||
|
||||
def rule_name(self) -> str:
|
||||
return "Unnecessary Copy Detection"
|
||||
|
||||
def severity(self) -> SeverityLevel:
|
||||
return SeverityLevel.LOW
|
||||
|
||||
def category(self) -> FindingCategory:
|
||||
return FindingCategory.PERFORMANCE
|
||||
|
||||
def analyze(
|
||||
self, source_code: str, file_path: Path, tree: tree_sitter.Tree
|
||||
) -> list[Finding]:
|
||||
findings = []
|
||||
copies = self._get_copy_calls(tree.root_node)
|
||||
|
||||
for copy in copies:
|
||||
if self._is_unnecessary(copy, source_code):
|
||||
line = self._get_line_number(copy, source_code)
|
||||
findings.append(
|
||||
Finding(
|
||||
rule_id=self.rule_id(),
|
||||
rule_name=self.rule_name(),
|
||||
severity=self.severity(),
|
||||
category=self.category(),
|
||||
message="Unnecessary list copy detected",
|
||||
suggestion="Avoid copying lists when not needed",
|
||||
file_path=file_path,
|
||||
line_number=line,
|
||||
column=self._get_column(copy),
|
||||
node=copy,
|
||||
)
|
||||
)
|
||||
return findings
|
||||
|
||||
def _get_copy_calls(self, node: tree_sitter.Node) -> list[tree_sitter.Node]:
|
||||
calls = []
|
||||
if hasattr(node, "type") and node.type == "call":
|
||||
func_name = self._get_function_name(node)
|
||||
if "copy" in func_name.lower() or "[:]" in self._get_node_text(node, ""):
|
||||
calls.append(node)
|
||||
if hasattr(node, "children"):
|
||||
for child in node.children:
|
||||
calls.extend(self._get_copy_calls(child))
|
||||
return calls
|
||||
|
||||
def _get_function_name(self, call: tree_sitter.Node) -> str:
|
||||
if hasattr(call, "children") and len(call.children) > 0:
|
||||
func = call.children[0]
|
||||
if hasattr(func, "text"):
|
||||
text = func.text
|
||||
return text.decode() if isinstance(text, bytes) else str(text)
|
||||
return ""
|
||||
|
||||
def _get_node_text(self, node: tree_sitter.Node, source_code: str) -> str:
|
||||
if hasattr(node, "start_byte") and hasattr(node, "end_byte"):
|
||||
return source_code[node.start_byte:node.end_byte]
|
||||
return ""
|
||||
|
||||
def _is_unnecessary(self, call: tree_sitter.Node, source_code: str) -> bool:
|
||||
call_text = self._get_node_text(call, source_code)
|
||||
unnecessary_patterns = [
|
||||
r"list\s*\(\s*list\s*\(",
|
||||
r"\[:\]\s*\[:\]",
|
||||
r"copy\s*\(\s*copy\s*\(",
|
||||
]
|
||||
return any(re.search(p, call_text) for p in unnecessary_patterns)
|
||||
|
||||
def _get_line_number(self, node: tree_sitter.Node, source_code: str) -> int:
|
||||
lines = source_code.split("\n")
|
||||
start_byte = node.start_byte if hasattr(node, "start_byte") else 0
|
||||
pos = 0
|
||||
for line_num, line_text in enumerate(lines, 1):
|
||||
if pos + len(line_text) >= start_byte:
|
||||
return line_num
|
||||
pos += len(line_text) + 1
|
||||
return 1
|
||||
|
||||
def _get_column(self, node: tree_sitter.Node) -> int:
|
||||
return node.start_column if hasattr(node, "start_column") else 0
|
||||
Reference in New Issue
Block a user