repopilot 0.11.0

Local-first CLI for repository audit, architecture risk detection, baseline tracking, and CI-friendly code review.
Documentation
use super::helpers::{is_js_or_ts, normalize, path_contains_component, push_unique};
use crate::audits::context::model::{FrameworkKind, LanguageKind};
use std::path::Path;

pub fn classify_frameworks(
    frameworks: &mut Vec<FrameworkKind>,
    path: &Path,
    content: &str,
    language: LanguageKind,
) {
    // JS/TS, Python, Go, Dart: all patterns are naturally lowercase in source — no allocation.
    // C# and JVM: use PascalCase/camelCase identifiers that need case-folding.
    if is_js_or_ts(language) {
        if is_react_native_content(content) {
            push_unique(frameworks, FrameworkKind::ReactNative);
        }

        if content.contains("from 'expo")
            || content.contains("from \"expo")
            || content.contains("expo-status-bar")
            || content.contains("expo-router")
        {
            push_unique(frameworks, FrameworkKind::Expo);
        }

        if is_react_file(path, content) {
            push_unique(frameworks, FrameworkKind::React);
        }

        if content.contains("next/")
            || (path_contains_component(path, &["pages", "app"]) && is_tsx_or_jsx_file(path))
        {
            push_unique(frameworks, FrameworkKind::NextJs);
        }

        if content.contains("from 'vue'")
            || content.contains("from \"vue\"")
            || content.contains("@vue/")
        {
            push_unique(frameworks, FrameworkKind::Vue);
        }

        if content.contains("@angular/") {
            push_unique(frameworks, FrameworkKind::Angular);
        }

        if content.contains("from 'svelte") || content.contains("from \"svelte") {
            push_unique(frameworks, FrameworkKind::Svelte);
        }

        if content.contains("@nestjs/") {
            push_unique(frameworks, FrameworkKind::NestJs);
        }

        if content.contains("express") {
            push_unique(frameworks, FrameworkKind::Express);
        }

        if content.contains("express")
            || content.contains("from 'node:")
            || content.contains("from \"node:")
            || content.contains("process.env")
            || content.contains("process.exit")
        {
            push_unique(frameworks, FrameworkKind::NodeJs);
        }
    }

    if language == LanguageKind::CSharp {
        // Compute lowercase only for C# where PascalCase identifiers need folding.
        let lower = content.to_lowercase();
        if is_unity_file(path, &lower) {
            push_unique(frameworks, FrameworkKind::Unity);
        }
        if is_dotnet_file(path, &lower) {
            push_unique(frameworks, FrameworkKind::DotNet);
        }
    }

    if language == LanguageKind::Python {
        if content.contains("django") {
            push_unique(frameworks, FrameworkKind::Django);
        }
        if content.contains("flask") {
            push_unique(frameworks, FrameworkKind::Flask);
        }
        if content.contains("fastapi") {
            push_unique(frameworks, FrameworkKind::FastApi);
        }
    }

    if language == LanguageKind::Go {
        if content.contains("github.com/gin-gonic/gin") {
            push_unique(frameworks, FrameworkKind::Gin);
        }
        if content.contains("github.com/labstack/echo") {
            push_unique(frameworks, FrameworkKind::Echo);
        }
        if content.contains("github.com/gofiber/fiber") {
            push_unique(frameworks, FrameworkKind::Fiber);
        }
    }

    if matches!(language, LanguageKind::Java | LanguageKind::Kotlin) {
        // Compute lowercase only for JVM where annotations use mixed case.
        let lower = content.to_lowercase();
        if lower.contains("org.springframework") || lower.contains("@springbootapplication") {
            push_unique(frameworks, FrameworkKind::Spring);
        }
        if lower.contains("android.")
            || lower.contains("androidx.")
            || path_contains_component(path, &["android"])
        {
            push_unique(frameworks, FrameworkKind::Android);
        }
    }

    if language == LanguageKind::Dart
        && (content.contains("package:flutter")
            || path_contains_component(path, &["lib", "widgets"]))
    {
        push_unique(frameworks, FrameworkKind::Flutter);
    }
}

fn is_react_native_content(content: &str) -> bool {
    content.contains("react-native")
        || content.contains("@react-native")
        || content.contains("from 'react-native'")
        || content.contains("from \"react-native\"")
}

fn is_react_file(path: &Path, content: &str) -> bool {
    is_tsx_or_jsx_file(path)
        || content.contains("from 'react'")
        || content.contains("from \"react\"")
        || content.contains("React.")
        || content.contains("React.FC")
}

pub fn is_tsx_or_jsx_file(path: &Path) -> bool {
    matches!(
        path.extension()
            .and_then(|extension| extension.to_str())
            .map(normalize)
            .as_deref(),
        Some("tsx") | Some("jsx")
    )
}

pub fn is_react_component_file(path: &Path, content: &str) -> bool {
    use super::helpers::is_pascal_case;

    if is_tsx_or_jsx_file(path) {
        return true;
    }

    let file_stem = path
        .file_stem()
        .and_then(|name| name.to_str())
        .unwrap_or_default();

    is_pascal_case(file_stem)
        && (content.contains("return <")
            || content.contains("</")
            || content.contains("React.FC")
            || content.contains("React.memo")
            || content.contains("memo(")
            || content.contains("forwardRef(")
            || content.contains("React.createElement"))
}

pub fn is_react_hook_file(path: &Path, content: &str) -> bool {
    let file_stem = path
        .file_stem()
        .and_then(|name| name.to_str())
        .unwrap_or_default();

    file_stem.starts_with("use")
        && file_stem
            .chars()
            .nth(3)
            .map(|character| character.is_uppercase())
            .unwrap_or(false)
        && (content.contains("useState")
            || content.contains("useEffect")
            || content.contains("useMemo")
            || content.contains("useCallback")
            || content.contains("useReducer")
            || content.contains("function use")
            || content.contains("const use"))
}

fn is_unity_file(path: &Path, content: &str) -> bool {
    content.contains("using unityengine")
        || content.contains("monobehaviour")
        || path_contains_component(path, &["assets"])
}

fn is_dotnet_file(path: &Path, content: &str) -> bool {
    content.contains("using microsoft.aspnetcore")
        || content.contains("webapplication.createbuilder")
        || content.contains("[apicontroller]")
        || path_contains_component(path, &["controllers", "services"])
}

pub fn is_dotnet_controller(path: &Path, content: &str) -> bool {
    path.file_name()
        .and_then(|name| name.to_str())
        .map(|name| name.ends_with("Controller.cs"))
        .unwrap_or(false)
        || content.contains("[apicontroller]")
        || content.contains(": controllerbase")
        || content.contains(": controller")
}

pub fn is_dotnet_service(path: &Path, content: &str) -> bool {
    path.file_name()
        .and_then(|name| name.to_str())
        .map(|name| name.ends_with("Service.cs"))
        .unwrap_or(false)
        || (content.contains("class ")
            && content.contains("service")
            && path_contains_component(path, &["services"]))
}