css_analyzer
¶
Full name: tenets.core.analysis.implementations.css_analyzer
css_analyzer¶
CSS code analyzer with preprocessor and framework support.
This module provides comprehensive analysis for CSS files, including support for CSS3, SCSS/Sass, Less, PostCSS, Tailwind CSS, UnoCSS, and other modern CSS frameworks.
Classes¶
CSSParser¶
Custom CSS parser for detailed analysis.
Source code in tenets/core/analysis/implementations/css_analyzer.py
def __init__(self, content: str, is_scss: bool = False):
self.content = content
self.is_scss = is_scss
self.rules = []
self.variables = {}
self.mixins = []
self.functions = []
self.keyframes = []
self.media_queries = []
self.supports_rules = []
self.custom_properties = {}
self.nesting_depth = 0
self.max_nesting = 0
Functions¶
parse¶
Parse CSS/SCSS content.
Source code in tenets/core/analysis/implementations/css_analyzer.py
def parse(self):
"""Parse CSS/SCSS content."""
# Remove comments for parsing
content = self._remove_comments(self.content)
# Extract different CSS elements
self._extract_variables(content)
self._extract_custom_properties(content)
self._extract_rules(content)
self._extract_media_queries(content)
self._extract_keyframes(content)
self._extract_supports_rules(content)
if self.is_scss:
self._extract_scss_features(content)
CSSAnalyzer¶
Bases: LanguageAnalyzer
CSS code analyzer with preprocessor and framework support.
Provides comprehensive analysis for CSS files including: - CSS3 features and properties - SCSS/Sass preprocessor features - Less preprocessor features - PostCSS plugins and features - Tailwind CSS utility classes - UnoCSS atomic CSS - CSS-in-JS patterns - CSS Modules - BEM, OOCSS, SMACSS methodologies - Performance metrics - Browser compatibility - Accessibility considerations - Design system patterns
Supports modern CSS development practices and frameworks.
Initialize the CSS analyzer with logger.
Source code in tenets/core/analysis/implementations/css_analyzer.py
def __init__(self):
"""Initialize the CSS analyzer with logger."""
self.logger = get_logger(__name__)
# Tailwind utility patterns
self.tailwind_patterns = self._load_tailwind_patterns()
# UnoCSS patterns
self.unocss_patterns = self._load_unocss_patterns()
# CSS framework patterns
self.framework_patterns = self._load_framework_patterns()
Functions¶
extract_imports¶
Extract import statements from CSS.
Handles: - @import statements - @use (Sass) - @forward (Sass) - url() functions - CSS Modules composes
PARAMETER | DESCRIPTION |
---|---|
content | CSS source code TYPE: |
file_path | Path to the file being analyzed TYPE: |
RETURNS | DESCRIPTION |
---|---|
List[ImportInfo] | List of ImportInfo objects with import details |
Source code in tenets/core/analysis/implementations/css_analyzer.py
def extract_imports(self, content: str, file_path: Path) -> List[ImportInfo]:
"""Extract import statements from CSS.
Handles:
- @import statements
- @use (Sass)
- @forward (Sass)
- url() functions
- CSS Modules composes
Args:
content: CSS source code
file_path: Path to the file being analyzed
Returns:
List of ImportInfo objects with import details
"""
imports = []
# Determine file type
ext = file_path.suffix.lower()
is_scss = ext in [".scss", ".sass"]
is_less = ext == ".less"
# @import statements
import_pattern = r'@import\s+(?:url\()?["\']([^"\']+)["\'](?:\))?(?:\s+([^;]+))?;'
for match in re.finditer(import_pattern, content):
import_path = match.group(1)
media_query = match.group(2)
imports.append(
ImportInfo(
module=import_path,
line=content[: match.start()].count("\n") + 1,
type="import",
is_relative=not import_path.startswith(("http://", "https://", "//")),
media_query=media_query.strip() if media_query else None,
category=self._categorize_css_import(import_path),
)
)
# @use statements (Sass)
if is_scss:
use_pattern = r'@use\s+["\']([^"\']+)["\'](?:\s+as\s+(\w+))?(?:\s+with\s*\(([^)]+)\))?;'
for match in re.finditer(use_pattern, content):
module_path = match.group(1)
namespace = match.group(2)
config = match.group(3)
imports.append(
ImportInfo(
module=module_path,
line=content[: match.start()].count("\n") + 1,
type="use",
is_relative=not module_path.startswith(("http://", "https://", "//")),
namespace=namespace,
config=config,
category=self._categorize_css_import(module_path),
)
)
# @forward statements (Sass)
forward_pattern = r'@forward\s+["\']([^"\']+)["\'](?:\s+(show|hide)\s+([^;]+))?;'
for match in re.finditer(forward_pattern, content):
module_path = match.group(1)
visibility_type = match.group(2)
visibility_items = match.group(3)
# Combine visibility type and items for easier testing
if visibility_type and visibility_items:
visibility = f"{visibility_type} {visibility_items.strip()}"
else:
visibility = None
imports.append(
ImportInfo(
module=module_path,
line=content[: match.start()].count("\n") + 1,
type="forward",
is_relative=not module_path.startswith(("http://", "https://", "//")),
visibility=visibility,
category=self._categorize_css_import(module_path),
)
)
# url() in properties (for fonts, images, etc.)
url_pattern = r'url\(["\']?([^"\')\s]+)["\']?\)'
for match in re.finditer(url_pattern, content):
url_path = match.group(1)
# Skip data URLs and already imported files
if url_path.startswith("data:") or any(imp.module == url_path for imp in imports):
continue
imports.append(
ImportInfo(
module=url_path,
line=content[: match.start()].count("\n") + 1,
type="url",
is_relative=not url_path.startswith(("http://", "https://", "//")),
category=self._categorize_url_import(url_path),
)
)
# CSS Modules composes
composes_pattern = r'composes:\s*([a-zA-Z0-9-_\s]+)\s+from\s+["\']([^"\']+)["\'];'
for match in re.finditer(composes_pattern, content):
classes = match.group(1)
module_path = match.group(2)
imports.append(
ImportInfo(
module=module_path,
line=content[: match.start()].count("\n") + 1,
type="composes",
is_relative=not module_path.startswith(("http://", "https://", "//")),
composes=classes.strip(),
# Alias for tests that expect composed_classes
visibility=None,
category="css_module",
)
)
# Backward compatibility: also attach composed_classes attribute dynamically
for imp in imports:
if imp.type == "composes" and getattr(imp, "composes", None):
# Some tests reference ImportInfo.composed_classes
try:
setattr(imp, "composed_classes", imp.composes)
except Exception:
pass
return imports
extract_exports¶
Extract exported elements from CSS.
In CSS context, exports are: - Classes that can be used by HTML - IDs - Custom properties (CSS variables) - Mixins (SCSS/Less) - Functions (SCSS) - Keyframe animations - Utility classes (Tailwind/UnoCSS)
PARAMETER | DESCRIPTION |
---|---|
content | CSS source code TYPE: |
file_path | Path to the file being analyzed TYPE: |
RETURNS | DESCRIPTION |
---|---|
List[Dict[str, Any]] | List of exported elements |
Source code in tenets/core/analysis/implementations/css_analyzer.py
def extract_exports(self, content: str, file_path: Path) -> List[Dict[str, Any]]:
"""Extract exported elements from CSS.
In CSS context, exports are:
- Classes that can be used by HTML
- IDs
- Custom properties (CSS variables)
- Mixins (SCSS/Less)
- Functions (SCSS)
- Keyframe animations
- Utility classes (Tailwind/UnoCSS)
Args:
content: CSS source code
file_path: Path to the file being analyzed
Returns:
List of exported elements
"""
exports = []
# Parse CSS
ext = file_path.suffix.lower()
is_scss = ext in [".scss", ".sass"]
parser = CSSParser(content, is_scss)
parser.parse()
# Export CSS classes (from selectors only)
classes: Set[str] = set()
for rule in parser.rules:
selector = rule.get("selector", "")
for match in re.finditer(r"\.([a-zA-Z0-9_\\:-]+)", selector):
class_name = match.group(1)
if class_name not in classes:
classes.add(class_name)
pos = content.find("." + class_name)
exports.append(
{
"name": class_name,
"type": "class",
"line": (content[:pos].count("\n") + 1) if pos != -1 else None,
}
)
# Export IDs (from selectors only, avoid hex colors)
ids: Set[str] = set()
for rule in parser.rules:
selector = rule.get("selector", "")
for match in re.finditer(r"#([a-zA-Z0-9_-]+)", selector):
id_name = match.group(1)
if id_name not in ids:
ids.add(id_name)
pos = content.find("#" + id_name)
exports.append(
{
"name": id_name,
"type": "id",
"line": (content[:pos].count("\n") + 1) if pos != -1 else None,
}
)
# Export custom properties
for prop_name, prop_value in parser.custom_properties.items():
exports.append(
{
"name": prop_name,
"type": "custom_property",
"value": prop_value,
}
)
# Export SCSS variables, mixins, functions
if is_scss:
for var_name, var_value in parser.variables.items():
exports.append(
{
"name": var_name,
"type": "scss_variable",
"value": var_value,
}
)
for mixin in parser.mixins:
exports.append(
{
"name": mixin["name"],
"type": "mixin",
"params": mixin["params"],
}
)
for func in parser.functions:
exports.append(
{
"name": func["name"],
"type": "function",
"params": func["params"],
}
)
# Export keyframes
for keyframe in parser.keyframes:
exports.append(
{
"name": keyframe["name"],
"type": "keyframe",
}
)
# Export utility classes (Tailwind/UnoCSS)
if self._is_utility_css(content):
utility_classes = self._extract_utility_classes(content)
for util_class in utility_classes:
exports.append(
{
"name": util_class,
"type": "utility_class",
"framework": self._detect_utility_framework(content),
}
)
return exports
extract_structure¶
Extract CSS document structure.
Extracts: - Rules and selectors - Media queries - CSS architecture patterns - Framework usage - Design tokens - Component structure
PARAMETER | DESCRIPTION |
---|---|
content | CSS source code TYPE: |
file_path | Path to the file being analyzed TYPE: |
RETURNS | DESCRIPTION |
---|---|
CodeStructure | CodeStructure object with extracted elements |
Source code in tenets/core/analysis/implementations/css_analyzer.py
def extract_structure(self, content: str, file_path: Path) -> CodeStructure:
"""Extract CSS document structure.
Extracts:
- Rules and selectors
- Media queries
- CSS architecture patterns
- Framework usage
- Design tokens
- Component structure
Args:
content: CSS source code
file_path: Path to the file being analyzed
Returns:
CodeStructure object with extracted elements
"""
structure = CodeStructure()
# Parse CSS
ext = file_path.suffix.lower()
is_scss = ext in [".scss", ".sass"]
is_less = ext == ".less"
parser = CSSParser(content, is_scss)
parser.parse()
# Store parsed data
structure.rules = parser.rules
structure.variables = parser.variables
structure.custom_properties = parser.custom_properties
structure.mixins = parser.mixins
structure.functions = parser.functions
structure.keyframes = parser.keyframes
structure.media_queries = parser.media_queries
structure.supports_rules = parser.supports_rules
structure.max_nesting = parser.max_nesting
# Detect CSS methodology
structure.uses_bem = self._detect_bem(content)
structure.uses_oocss = self._detect_oocss(content)
structure.uses_smacss = self._detect_smacss(content)
structure.uses_atomic = self._detect_atomic_css(content)
# Detect frameworks
structure.is_tailwind = self._detect_tailwind(content, file_path)
structure.is_unocss = self._detect_unocss(content, file_path)
structure.is_bootstrap = self._detect_bootstrap(content)
structure.is_bulma = self._detect_bulma(content)
structure.is_material = self._detect_material(content)
# Count selectors by type (from selectors only)
selectors_joined = ",".join(rule.get("selector", "") for rule in parser.rules)
structure.element_selectors = len(
re.findall(r"(?:(?<=^)|(?<=[\s>+~,(]))[a-zA-Z][a-zA-Z0-9-]*", selectors_joined)
)
structure.class_selectors = len(re.findall(r"\.[a-zA-Z0-9_\\:-]+", selectors_joined))
structure.id_selectors = len(re.findall(r"#[a-zA-Z0-9_-]+", selectors_joined))
structure.attribute_selectors = len(re.findall(r"\[[^\]]+\]", selectors_joined))
structure.pseudo_classes = len(re.findall(r":(?!:)[a-z-]+(?:\([^)]*\))?", selectors_joined))
structure.pseudo_elements = len(re.findall(r"::[a-z-]+", selectors_joined))
# Count CSS3 features
structure.flexbox_usage = len(re.findall(r"display\s*:\s*(?:inline-)?flex", content))
structure.grid_usage = len(re.findall(r"display\s*:\s*grid", content))
structure.custom_property_usage = len(re.findall(r"var\(--[^)]+\)", content))
structure.calc_usage = len(re.findall(r"calc\([^)]+\)", content))
structure.transform_usage = len(re.findall(r"transform\s*:", content))
structure.transition_usage = len(re.findall(r"transition\s*:", content))
structure.animation_usage = len(re.findall(r"animation\s*:", content))
# Count responsive features
structure.media_query_count = len(parser.media_queries)
structure.viewport_units = len(re.findall(r"\d+(?:vw|vh|vmin|vmax)\b", content))
structure.container_queries = len(re.findall(r"@container\s+", content))
# Count modern CSS features
structure.css_nesting = len(
re.findall(r"&\s*[{:.]", content)
) + self._count_nested_selectors(content)
structure.has_layers = bool(re.search(r"@layer\s+", content))
structure.has_cascade_layers = len(re.findall(r"@layer\s+[a-z-]+\s*[{,]", content))
# Design system detection
structure.has_design_tokens = self._detect_design_tokens(content)
structure.color_variables = self._count_color_variables(parser.custom_properties)
structure.spacing_variables = self._count_spacing_variables(parser.custom_properties)
structure.typography_variables = self._count_typography_variables(parser.custom_properties)
# Component-based structure
structure.component_count = self._count_components(content)
structure.utility_count = self._count_utilities(content)
# PostCSS features
structure.uses_postcss = self._detect_postcss(content, file_path)
structure.postcss_plugins = self._detect_postcss_plugins(content)
# CSS-in-JS patterns
structure.is_css_modules = self._detect_css_modules(content, file_path)
structure.is_styled_components = self._detect_styled_components(content)
# Performance indicators
structure.unused_variables = self._find_unused_variables(content, parser)
structure.duplicate_properties = self._find_duplicate_properties(parser.rules)
structure.vendor_prefixes = len(re.findall(r"-(?:webkit|moz|ms|o)-", content))
# Accessibility
structure.focus_styles = len(re.findall(r":focus\s*[{,]", content))
structure.focus_visible = len(re.findall(r":focus-visible\s*[{,]", content))
structure.reduced_motion = len(re.findall(r"prefers-reduced-motion", content))
structure.high_contrast = len(re.findall(r"prefers-contrast", content))
structure.color_scheme = len(re.findall(r"prefers-color-scheme", content))
return structure
calculate_complexity¶
Calculate complexity metrics for CSS.
Calculates: - Selector complexity - Specificity metrics - Rule complexity - Nesting depth - Framework complexity - Performance score - Maintainability index
PARAMETER | DESCRIPTION |
---|---|
content | CSS source code TYPE: |
file_path | Path to the file being analyzed TYPE: |
RETURNS | DESCRIPTION |
---|---|
ComplexityMetrics | ComplexityMetrics object with calculated metrics |
Source code in tenets/core/analysis/implementations/css_analyzer.py
def calculate_complexity(self, content: str, file_path: Path) -> ComplexityMetrics:
"""Calculate complexity metrics for CSS.
Calculates:
- Selector complexity
- Specificity metrics
- Rule complexity
- Nesting depth
- Framework complexity
- Performance score
- Maintainability index
Args:
content: CSS source code
file_path: Path to the file being analyzed
Returns:
ComplexityMetrics object with calculated metrics
"""
metrics = ComplexityMetrics()
# Parse CSS
ext = file_path.suffix.lower()
is_scss = ext in [".scss", ".sass"]
parser = CSSParser(content, is_scss)
parser.parse()
# Basic metrics
lines = content.split("\n")
metrics.line_count = len(lines)
metrics.code_lines = len([l for l in lines if l.strip() and not l.strip().startswith("//")])
metrics.comment_lines = len(re.findall(r"/\*.*?\*/", content, re.DOTALL))
if is_scss:
metrics.comment_lines += len([l for l in lines if l.strip().startswith("//")])
# Rule metrics
metrics.total_rules = len(parser.rules)
metrics.total_selectors = sum(len(rule["selector"].split(",")) for rule in parser.rules)
# Calculate average specificity
total_specificity = [0, 0, 0]
max_specificity = [0, 0, 0]
for rule in parser.rules:
spec = rule["specificity"]
total_specificity[0] += spec[0]
total_specificity[1] += spec[1]
total_specificity[2] += spec[2]
if spec[0] > max_specificity[0]:
max_specificity = spec
elif spec[0] == max_specificity[0] and spec[1] > max_specificity[1]:
max_specificity = spec
elif (
spec[0] == max_specificity[0]
and spec[1] == max_specificity[1]
and spec[2] > max_specificity[2]
):
max_specificity = spec
if metrics.total_rules > 0:
metrics.avg_specificity = [
total_specificity[0] / metrics.total_rules,
total_specificity[1] / metrics.total_rules,
total_specificity[2] / metrics.total_rules,
]
else:
metrics.avg_specificity = [0, 0, 0]
metrics.max_specificity = max_specificity
# Selector complexity
metrics.complex_selectors = 0
metrics.overqualified_selectors = 0
for rule in parser.rules:
selector = rule["selector"]
# Complex selector (too many parts)
if len(selector.split()) > 3:
metrics.complex_selectors += 1
# Overqualified (element with class/id)
if re.search(r"[a-z]+\.[a-z-]+|[a-z]+#[a-z-]+", selector, re.IGNORECASE):
metrics.overqualified_selectors += 1
# Important usage
metrics.important_count = len(re.findall(r"!important", content))
# Media query complexity
metrics.media_query_count = len(parser.media_queries)
metrics.media_query_complexity = sum(
len(mq["condition"].split("and")) for mq in parser.media_queries
)
# Nesting depth (for SCSS)
metrics.max_nesting_depth = parser.max_nesting
# Color usage
metrics.unique_colors = len(
set(
re.findall(
r"#[0-9a-fA-F]{3,8}|rgb\([^)]+\)|rgba\([^)]+\)|hsl\([^)]+\)|hsla\([^)]+\)",
content,
)
)
)
# Font usage
metrics.unique_fonts = len(set(re.findall(r"font-family\s*:\s*([^;]+);", content)))
# Z-index usage
z_indices = re.findall(r"z-index\s*:\s*(-?\d+)", content)
metrics.z_index_count = len(z_indices)
if z_indices:
metrics.max_z_index = max(int(z) for z in z_indices)
else:
metrics.max_z_index = 0
# File size metrics
metrics.file_size = len(content.encode("utf-8"))
metrics.gzip_ratio = self._estimate_gzip_ratio(content)
# Framework-specific metrics
if self._detect_tailwind(content, file_path):
metrics.tailwind_classes = self._count_tailwind_classes(content)
metrics.custom_utilities = self._count_custom_utilities(content)
if self._detect_unocss(content, file_path):
metrics.unocss_classes = self._count_unocss_classes(content)
# Calculate CSS complexity score
complexity_score = (
metrics.total_rules * 0.1
+ metrics.complex_selectors * 2
+ metrics.overqualified_selectors * 1.5
+ metrics.important_count * 3
+ metrics.max_nesting_depth * 1
+ (metrics.max_specificity[0] * 10) # IDs weighted heavily
+ (metrics.max_specificity[1] * 2) # Classes
+ (metrics.max_specificity[2] * 0.5) # Elements
)
metrics.complexity_score = complexity_score
# Performance score
performance_score = 100
# Deduct for complexity
performance_score -= min(30, complexity_score / 10)
# Deduct for !important
performance_score -= min(20, metrics.important_count * 2)
# Deduct for deep nesting
performance_score -= min(10, metrics.max_nesting_depth * 2)
# Deduct for excessive specificity
performance_score -= min(10, metrics.max_specificity[0] * 5)
# Bonus for CSS variables usage
if len(parser.custom_properties) > 0:
performance_score += min(10, len(parser.custom_properties) * 0.5)
metrics.performance_score = max(0, performance_score)
# Calculate maintainability index
import math
if metrics.code_lines > 0:
# Factors affecting CSS maintainability
specificity_factor = 1 - (sum(metrics.avg_specificity) * 0.1)
important_factor = 1 - (metrics.important_count * 0.02)
nesting_factor = 1 - (metrics.max_nesting_depth * 0.05)
organization_factor = 1 if len(parser.custom_properties) > 0 else 0.8
mi = (
171
- 5.2 * math.log(max(1, metrics.total_rules))
- 0.23 * complexity_score
- 16.2 * math.log(max(1, metrics.code_lines))
+ 20 * specificity_factor
+ 10 * important_factor
+ 10 * nesting_factor
+ 10 * organization_factor
)
metrics.maintainability_index = max(0, min(100, mi))
else:
metrics.maintainability_index = 100
return metrics