fix: Address high-severity bandit issues
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
"""
|
||||
React Project Analyzer
|
||||
React Project Analyzer.
|
||||
|
||||
Analyzes React codebases to extract component information,
|
||||
detect patterns, and identify style usage.
|
||||
@@ -7,90 +7,58 @@ detect patterns, and identify style usage.
|
||||
|
||||
import re
|
||||
from pathlib import Path
|
||||
from typing import List, Dict, Any, Optional, Set, Tuple
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
from .base import (
|
||||
ComponentInfo,
|
||||
Location,
|
||||
StylePattern,
|
||||
StylingApproach,
|
||||
)
|
||||
from typing import Any, Dict, List, Optional, Set
|
||||
|
||||
from .base import ComponentInfo, Location
|
||||
|
||||
# Patterns for React component detection
|
||||
FUNCTIONAL_COMPONENT = re.compile(
|
||||
r'(?:export\s+)?(?:const|let|var|function)\s+([A-Z][A-Za-z0-9]*)\s*(?::\s*(?:React\.)?FC)?'
|
||||
r'\s*(?:=\s*(?:\([^)]*\)|[a-zA-Z_]\w*)\s*=>|\()',
|
||||
re.MULTILINE
|
||||
r"(?:export\s+)?(?:const|let|var|function)\s+([A-Z][A-Za-z0-9]*)\s*(?::\s*(?:React\.)?FC)?"
|
||||
r"\s*(?:=\s*(?:\([^)]*\)|[a-zA-Z_]\w*)\s*=>|\()",
|
||||
re.MULTILINE,
|
||||
)
|
||||
|
||||
CLASS_COMPONENT = re.compile(
|
||||
r'class\s+([A-Z][A-Za-z0-9]*)\s+extends\s+(?:React\.)?(?:Component|PureComponent)',
|
||||
re.MULTILINE
|
||||
r"class\s+([A-Z][A-Za-z0-9]*)\s+extends\s+(?:React\.)?(?:Component|PureComponent)", re.MULTILINE
|
||||
)
|
||||
|
||||
FORWARD_REF = re.compile(
|
||||
r'(?:export\s+)?(?:const|let)\s+([A-Z][A-Za-z0-9]*)\s*=\s*(?:React\.)?forwardRef',
|
||||
re.MULTILINE
|
||||
r"(?:export\s+)?(?:const|let)\s+([A-Z][A-Za-z0-9]*)\s*=\s*(?:React\.)?forwardRef", re.MULTILINE
|
||||
)
|
||||
|
||||
MEMO_COMPONENT = re.compile(
|
||||
r'(?:export\s+)?(?:const|let)\s+([A-Z][A-Za-z0-9]*)\s*=\s*(?:React\.)?memo\(',
|
||||
re.MULTILINE
|
||||
r"(?:export\s+)?(?:const|let)\s+([A-Z][A-Za-z0-9]*)\s*=\s*(?:React\.)?memo\(", re.MULTILINE
|
||||
)
|
||||
|
||||
# Import patterns
|
||||
IMPORT_PATTERN = re.compile(
|
||||
r'import\s+(?:\{[^}]+\}|\*\s+as\s+\w+|\w+)\s+from\s+["\']([^"\']+)["\']',
|
||||
re.MULTILINE
|
||||
r'import\s+(?:\{[^}]+\}|\*\s+as\s+\w+|\w+)\s+from\s+["\']([^"\']+)["\']', re.MULTILINE
|
||||
)
|
||||
|
||||
STYLE_IMPORT = re.compile(
|
||||
r'import\s+(?:(\w+)\s+from\s+)?["\']([^"\']+\.(?:css|scss|sass|less|styl))["\']',
|
||||
re.MULTILINE
|
||||
r'import\s+(?:(\w+)\s+from\s+)?["\']([^"\']+\.(?:css|scss|sass|less|styl))["\']', re.MULTILINE
|
||||
)
|
||||
|
||||
# Inline style patterns
|
||||
INLINE_STYLE_OBJECT = re.compile(
|
||||
r'style\s*=\s*\{\s*\{([^}]+)\}\s*\}',
|
||||
re.MULTILINE | re.DOTALL
|
||||
)
|
||||
INLINE_STYLE_OBJECT = re.compile(r"style\s*=\s*\{\s*\{([^}]+)\}\s*\}", re.MULTILINE | re.DOTALL)
|
||||
|
||||
INLINE_STYLE_VAR = re.compile(
|
||||
r'style\s*=\s*\{(\w+)\}',
|
||||
re.MULTILINE
|
||||
)
|
||||
INLINE_STYLE_VAR = re.compile(r"style\s*=\s*\{(\w+)\}", re.MULTILINE)
|
||||
|
||||
# Props extraction
|
||||
PROPS_DESTRUCTURE = re.compile(
|
||||
r'\(\s*\{\s*([^}]+)\s*\}\s*(?::\s*[^)]+)?\)',
|
||||
re.MULTILINE
|
||||
)
|
||||
PROPS_DESTRUCTURE = re.compile(r"\(\s*\{\s*([^}]+)\s*\}\s*(?::\s*[^)]+)?\)", re.MULTILINE)
|
||||
|
||||
PROPS_INTERFACE = re.compile(
|
||||
r'interface\s+\w*Props\s*\{([^}]+)\}',
|
||||
re.MULTILINE | re.DOTALL
|
||||
)
|
||||
PROPS_INTERFACE = re.compile(r"interface\s+\w*Props\s*\{([^}]+)\}", re.MULTILINE | re.DOTALL)
|
||||
|
||||
PROPS_TYPE = re.compile(
|
||||
r'type\s+\w*Props\s*=\s*\{([^}]+)\}',
|
||||
re.MULTILINE | re.DOTALL
|
||||
)
|
||||
PROPS_TYPE = re.compile(r"type\s+\w*Props\s*=\s*\{([^}]+)\}", re.MULTILINE | re.DOTALL)
|
||||
|
||||
|
||||
class ReactAnalyzer:
|
||||
"""
|
||||
Analyzes React projects for component structure and style usage.
|
||||
"""
|
||||
"""Analyzes React projects for component structure and style usage."""
|
||||
|
||||
def __init__(self, root_path: str):
|
||||
self.root = Path(root_path).resolve()
|
||||
|
||||
async def analyze(
|
||||
self,
|
||||
component_files: Optional[List[Path]] = None
|
||||
) -> List[ComponentInfo]:
|
||||
async def analyze(self, component_files: Optional[List[Path]] = None) -> List[ComponentInfo]:
|
||||
"""
|
||||
Analyze React components in the project.
|
||||
|
||||
@@ -110,7 +78,7 @@ class ReactAnalyzer:
|
||||
try:
|
||||
file_components = await self._analyze_file(file_path)
|
||||
components.extend(file_components)
|
||||
except Exception as e:
|
||||
except Exception:
|
||||
# Log error but continue
|
||||
continue
|
||||
|
||||
@@ -118,21 +86,23 @@ class ReactAnalyzer:
|
||||
|
||||
def _find_component_files(self) -> List[Path]:
|
||||
"""Find all potential React component files."""
|
||||
skip_dirs = {'node_modules', '.git', 'dist', 'build', '.next'}
|
||||
skip_dirs = {"node_modules", ".git", "dist", "build", ".next"}
|
||||
component_files = []
|
||||
|
||||
for ext in ['*.jsx', '*.tsx']:
|
||||
for ext in ["*.jsx", "*.tsx"]:
|
||||
for path in self.root.rglob(ext):
|
||||
if not any(skip in path.parts for skip in skip_dirs):
|
||||
component_files.append(path)
|
||||
|
||||
# Also check .js/.ts files that look like components
|
||||
for ext in ['*.js', '*.ts']:
|
||||
for ext in ["*.js", "*.ts"]:
|
||||
for path in self.root.rglob(ext):
|
||||
if any(skip in path.parts for skip in skip_dirs):
|
||||
continue
|
||||
# Skip config and utility files
|
||||
if any(x in path.name.lower() for x in ['config', 'util', 'helper', 'hook', 'context']):
|
||||
if any(
|
||||
x in path.name.lower() for x in ["config", "util", "helper", "hook", "context"]
|
||||
):
|
||||
continue
|
||||
# Check if PascalCase (likely component)
|
||||
if path.stem[0].isupper():
|
||||
@@ -142,7 +112,7 @@ class ReactAnalyzer:
|
||||
|
||||
async def _analyze_file(self, file_path: Path) -> List[ComponentInfo]:
|
||||
"""Analyze a single file for React components."""
|
||||
content = file_path.read_text(encoding='utf-8', errors='ignore')
|
||||
content = file_path.read_text(encoding="utf-8", errors="ignore")
|
||||
components = []
|
||||
|
||||
# Find all components in the file
|
||||
@@ -152,22 +122,22 @@ class ReactAnalyzer:
|
||||
for match in FUNCTIONAL_COMPONENT.finditer(content):
|
||||
name = match.group(1)
|
||||
if self._is_valid_component_name(name):
|
||||
component_matches.append((name, 'functional', match.start()))
|
||||
component_matches.append((name, "functional", match.start()))
|
||||
|
||||
# Class components
|
||||
for match in CLASS_COMPONENT.finditer(content):
|
||||
name = match.group(1)
|
||||
component_matches.append((name, 'class', match.start()))
|
||||
component_matches.append((name, "class", match.start()))
|
||||
|
||||
# forwardRef components
|
||||
for match in FORWARD_REF.finditer(content):
|
||||
name = match.group(1)
|
||||
component_matches.append((name, 'forwardRef', match.start()))
|
||||
component_matches.append((name, "forwardRef", match.start()))
|
||||
|
||||
# memo components
|
||||
for match in MEMO_COMPONENT.finditer(content):
|
||||
name = match.group(1)
|
||||
component_matches.append((name, 'memo', match.start()))
|
||||
component_matches.append((name, "memo", match.start()))
|
||||
|
||||
# Dedupe by name (keep first occurrence)
|
||||
seen_names = set()
|
||||
@@ -193,19 +163,21 @@ class ReactAnalyzer:
|
||||
# Check if component has styles
|
||||
has_styles = bool(style_files) or bool(inline_styles)
|
||||
|
||||
components.append(ComponentInfo(
|
||||
name=name,
|
||||
path=str(file_path.relative_to(self.root)),
|
||||
type=comp_type,
|
||||
props=props,
|
||||
has_styles=has_styles,
|
||||
style_files=style_files,
|
||||
inline_style_count=len(inline_styles),
|
||||
imports=imports,
|
||||
exports=self._find_exports(content, name),
|
||||
children=children,
|
||||
line_count=content.count('\n') + 1,
|
||||
))
|
||||
components.append(
|
||||
ComponentInfo(
|
||||
name=name,
|
||||
path=str(file_path.relative_to(self.root)),
|
||||
type=comp_type,
|
||||
props=props,
|
||||
has_styles=has_styles,
|
||||
style_files=style_files,
|
||||
inline_style_count=len(inline_styles),
|
||||
imports=imports,
|
||||
exports=self._find_exports(content, name),
|
||||
children=children,
|
||||
line_count=content.count("\n") + 1,
|
||||
)
|
||||
)
|
||||
|
||||
return components
|
||||
|
||||
@@ -217,10 +189,22 @@ class ReactAnalyzer:
|
||||
|
||||
# Filter out common non-component patterns
|
||||
invalid_names = {
|
||||
'React', 'Component', 'PureComponent', 'Fragment',
|
||||
'Suspense', 'Provider', 'Consumer', 'Context',
|
||||
'Error', 'ErrorBoundary', 'Wrapper', 'Container',
|
||||
'Props', 'State', 'Type', 'Interface',
|
||||
"React",
|
||||
"Component",
|
||||
"PureComponent",
|
||||
"Fragment",
|
||||
"Suspense",
|
||||
"Provider",
|
||||
"Consumer",
|
||||
"Context",
|
||||
"Error",
|
||||
"ErrorBoundary",
|
||||
"Wrapper",
|
||||
"Container",
|
||||
"Props",
|
||||
"State",
|
||||
"Type",
|
||||
"Interface",
|
||||
}
|
||||
|
||||
return name not in invalid_names
|
||||
@@ -231,7 +215,7 @@ class ReactAnalyzer:
|
||||
for match in IMPORT_PATTERN.finditer(content):
|
||||
import_path = match.group(1)
|
||||
# Skip node_modules style imports for brevity
|
||||
if not import_path.startswith('.') and '/' not in import_path:
|
||||
if not import_path.startswith(".") and "/" not in import_path:
|
||||
continue
|
||||
imports.append(import_path)
|
||||
return imports
|
||||
@@ -250,11 +234,13 @@ class ReactAnalyzer:
|
||||
|
||||
# style={{ ... }}
|
||||
for match in INLINE_STYLE_OBJECT.finditer(content):
|
||||
line = content[:match.start()].count('\n') + 1
|
||||
locations.append(Location(
|
||||
file_path="", # Will be set by caller
|
||||
line=line,
|
||||
))
|
||||
line = content[: match.start()].count("\n") + 1
|
||||
locations.append(
|
||||
Location(
|
||||
file_path="", # Will be set by caller
|
||||
line=line,
|
||||
)
|
||||
)
|
||||
|
||||
return locations
|
||||
|
||||
@@ -266,7 +252,7 @@ class ReactAnalyzer:
|
||||
for match in PROPS_DESTRUCTURE.finditer(content):
|
||||
props_str = match.group(1)
|
||||
# Extract prop names from destructuring
|
||||
for prop in re.findall(r'(\w+)(?:\s*[=:])?', props_str):
|
||||
for prop in re.findall(r"(\w+)(?:\s*[=:])?", props_str):
|
||||
if prop and not prop[0].isupper(): # Skip types
|
||||
props.add(prop)
|
||||
|
||||
@@ -275,28 +261,24 @@ class ReactAnalyzer:
|
||||
for match in pattern.finditer(content):
|
||||
props_str = match.group(1)
|
||||
# Extract prop names
|
||||
for line in props_str.split('\n'):
|
||||
prop_match = re.match(r'\s*(\w+)\s*[?:]', line)
|
||||
for line in props_str.split("\n"):
|
||||
prop_match = re.match(r"\s*(\w+)\s*[?:]", line)
|
||||
if prop_match:
|
||||
props.add(prop_match.group(1))
|
||||
|
||||
return list(props)
|
||||
|
||||
def _find_child_components(
|
||||
self,
|
||||
content: str,
|
||||
current_components: Set[str]
|
||||
) -> List[str]:
|
||||
def _find_child_components(self, content: str, current_components: Set[str]) -> List[str]:
|
||||
"""Find child components used in JSX."""
|
||||
children = set()
|
||||
|
||||
# Find JSX elements that look like components (PascalCase)
|
||||
jsx_pattern = re.compile(r'<([A-Z][A-Za-z0-9]*)')
|
||||
jsx_pattern = re.compile(r"<([A-Z][A-Za-z0-9]*)")
|
||||
for match in jsx_pattern.finditer(content):
|
||||
component_name = match.group(1)
|
||||
# Skip current file's components and React built-ins
|
||||
if component_name not in current_components:
|
||||
if component_name not in {'Fragment', 'Suspense', 'Provider'}:
|
||||
if component_name not in {"Fragment", "Suspense", "Provider"}:
|
||||
children.add(component_name)
|
||||
|
||||
return list(children)
|
||||
@@ -306,16 +288,16 @@ class ReactAnalyzer:
|
||||
exports = []
|
||||
|
||||
# Default export
|
||||
if re.search(rf'export\s+default\s+{component_name}\b', content):
|
||||
exports.append('default')
|
||||
if re.search(rf'export\s+default\s+(?:function|const)\s+{component_name}\b', content):
|
||||
exports.append('default')
|
||||
if re.search(rf"export\s+default\s+{component_name}\b", content):
|
||||
exports.append("default")
|
||||
if re.search(rf"export\s+default\s+(?:function|const)\s+{component_name}\b", content):
|
||||
exports.append("default")
|
||||
|
||||
# Named export
|
||||
if re.search(rf'export\s+(?:const|function|class)\s+{component_name}\b', content):
|
||||
exports.append('named')
|
||||
if re.search(r'export\s*\{[^}]*\b' + re.escape(component_name) + r'\b[^}]*\}', content):
|
||||
exports.append('named')
|
||||
if re.search(rf"export\s+(?:const|function|class)\s+{component_name}\b", content):
|
||||
exports.append("named")
|
||||
if re.search(r"export\s*\{[^}]*\b" + re.escape(component_name) + r"\b[^}]*\}", content):
|
||||
exports.append("named")
|
||||
|
||||
return exports
|
||||
|
||||
@@ -332,39 +314,44 @@ class ReactAnalyzer:
|
||||
search_path = Path(path) if path else self.root
|
||||
results = []
|
||||
|
||||
for ext in ['*.jsx', '*.tsx', '*.js', '*.ts']:
|
||||
for ext in ["*.jsx", "*.tsx", "*.js", "*.ts"]:
|
||||
for file_path in search_path.rglob(ext):
|
||||
if any(skip in file_path.parts for skip in
|
||||
{'node_modules', '.git', 'dist', 'build'}):
|
||||
if any(
|
||||
skip in file_path.parts for skip in {"node_modules", ".git", "dist", "build"}
|
||||
):
|
||||
continue
|
||||
|
||||
try:
|
||||
content = file_path.read_text(encoding='utf-8', errors='ignore')
|
||||
content = file_path.read_text(encoding="utf-8", errors="ignore")
|
||||
|
||||
# Find style={{ ... }}
|
||||
for match in INLINE_STYLE_OBJECT.finditer(content):
|
||||
line = content[:match.start()].count('\n') + 1
|
||||
line = content[: match.start()].count("\n") + 1
|
||||
style_content = match.group(1).strip()
|
||||
|
||||
results.append({
|
||||
'file': str(file_path.relative_to(self.root)),
|
||||
'line': line,
|
||||
'content': style_content[:200],
|
||||
'type': 'object',
|
||||
})
|
||||
results.append(
|
||||
{
|
||||
"file": str(file_path.relative_to(self.root)),
|
||||
"line": line,
|
||||
"content": style_content[:200],
|
||||
"type": "object",
|
||||
}
|
||||
)
|
||||
|
||||
# Find style={variable}
|
||||
for match in INLINE_STYLE_VAR.finditer(content):
|
||||
line = content[:match.start()].count('\n') + 1
|
||||
line = content[: match.start()].count("\n") + 1
|
||||
var_name = match.group(1)
|
||||
|
||||
results.append({
|
||||
'file': str(file_path.relative_to(self.root)),
|
||||
'line': line,
|
||||
'content': f'style={{{var_name}}}',
|
||||
'type': 'variable',
|
||||
'variable': var_name,
|
||||
})
|
||||
results.append(
|
||||
{
|
||||
"file": str(file_path.relative_to(self.root)),
|
||||
"line": line,
|
||||
"content": f"style={{{var_name}}}",
|
||||
"type": "variable",
|
||||
"variable": var_name,
|
||||
}
|
||||
)
|
||||
|
||||
except Exception:
|
||||
continue
|
||||
@@ -392,48 +379,50 @@ class ReactAnalyzer:
|
||||
Returns dict with pattern types and their occurrences.
|
||||
"""
|
||||
patterns = {
|
||||
'inline_styles': [],
|
||||
'css_modules': [],
|
||||
'styled_components': [],
|
||||
'emotion': [],
|
||||
'tailwind': [],
|
||||
'css_classes': [],
|
||||
"inline_styles": [],
|
||||
"css_modules": [],
|
||||
"styled_components": [],
|
||||
"emotion": [],
|
||||
"tailwind": [],
|
||||
"css_classes": [],
|
||||
}
|
||||
|
||||
component_files = self._find_component_files()
|
||||
|
||||
for file_path in component_files:
|
||||
try:
|
||||
content = file_path.read_text(encoding='utf-8', errors='ignore')
|
||||
content = file_path.read_text(encoding="utf-8", errors="ignore")
|
||||
rel_path = str(file_path.relative_to(self.root))
|
||||
|
||||
# CSS Modules
|
||||
if re.search(r'import\s+\w+\s+from\s+["\'].*\.module\.', content):
|
||||
patterns['css_modules'].append({'file': rel_path})
|
||||
patterns["css_modules"].append({"file": rel_path})
|
||||
|
||||
# styled-components
|
||||
if re.search(r'styled\.|from\s+["\']styled-components', content):
|
||||
patterns['styled_components'].append({'file': rel_path})
|
||||
patterns["styled_components"].append({"file": rel_path})
|
||||
|
||||
# Emotion
|
||||
if re.search(r'@emotion|css`', content):
|
||||
patterns['emotion'].append({'file': rel_path})
|
||||
if re.search(r"@emotion|css`", content):
|
||||
patterns["emotion"].append({"file": rel_path})
|
||||
|
||||
# Tailwind (className with utility classes)
|
||||
if re.search(r'className\s*=\s*["\'][^"\']*(?:flex|grid|p-\d|m-\d|bg-)', content):
|
||||
patterns['tailwind'].append({'file': rel_path})
|
||||
patterns["tailwind"].append({"file": rel_path})
|
||||
|
||||
# Regular CSS classes
|
||||
if re.search(r'className\s*=\s*["\'][a-zA-Z]', content):
|
||||
patterns['css_classes'].append({'file': rel_path})
|
||||
patterns["css_classes"].append({"file": rel_path})
|
||||
|
||||
# Inline styles
|
||||
for match in INLINE_STYLE_OBJECT.finditer(content):
|
||||
line = content[:match.start()].count('\n') + 1
|
||||
patterns['inline_styles'].append({
|
||||
'file': rel_path,
|
||||
'line': line,
|
||||
})
|
||||
line = content[: match.start()].count("\n") + 1
|
||||
patterns["inline_styles"].append(
|
||||
{
|
||||
"file": rel_path,
|
||||
"line": line,
|
||||
}
|
||||
)
|
||||
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
Reference in New Issue
Block a user