Skip to content

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

Python
CSSParser(content: str, is_scss: bool = False)

Custom CSS parser for detailed analysis.

Source code in tenets/core/analysis/implementations/css_analyzer.py
Python
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
Python
parse()

Parse CSS/SCSS content.

Source code in tenets/core/analysis/implementations/css_analyzer.py
Python
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

Python
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
Python
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
Python
extract_imports(content: str, file_path: Path) -> List[ImportInfo]

Extract import statements from CSS.

Handles: - @import statements - @use (Sass) - @forward (Sass) - url() functions - CSS Modules composes

PARAMETERDESCRIPTION
content

CSS source code

TYPE:str

file_path

Path to the file being analyzed

TYPE:Path

RETURNSDESCRIPTION
List[ImportInfo]

List of ImportInfo objects with import details

Source code in tenets/core/analysis/implementations/css_analyzer.py
Python
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
Python
extract_exports(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)

PARAMETERDESCRIPTION
content

CSS source code

TYPE:str

file_path

Path to the file being analyzed

TYPE:Path

RETURNSDESCRIPTION
List[Dict[str, Any]]

List of exported elements

Source code in tenets/core/analysis/implementations/css_analyzer.py
Python
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
Python
extract_structure(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

PARAMETERDESCRIPTION
content

CSS source code

TYPE:str

file_path

Path to the file being analyzed

TYPE:Path

RETURNSDESCRIPTION
CodeStructure

CodeStructure object with extracted elements

Source code in tenets/core/analysis/implementations/css_analyzer.py
Python
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
Python
calculate_complexity(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

PARAMETERDESCRIPTION
content

CSS source code

TYPE:str

file_path

Path to the file being analyzed

TYPE:Path

RETURNSDESCRIPTION
ComplexityMetrics

ComplexityMetrics object with calculated metrics

Source code in tenets/core/analysis/implementations/css_analyzer.py
Python
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

Functions