repopilot 0.10.0

Local-first CLI for repository audit, architecture risk detection, baseline tracking, and CI-friendly code review.
Documentation
use crate::audits::framework::js_common::{is_comment_line, is_js_file};
use crate::audits::traits::ProjectAudit;
use crate::findings::types::{Evidence, Finding, FindingCategory, Severity};
use crate::scan::config::ScanConfig;
use crate::scan::facts::ScanFacts;

pub struct RnInlineStyleAudit;

impl ProjectAudit for RnInlineStyleAudit {
    fn audit(&self, facts: &ScanFacts, _config: &ScanConfig) -> Vec<Finding> {
        let mut findings = Vec::new();

        for file in &facts.files {
            if !is_js_file(&file.path) {
                continue;
            }

            let content = match std::fs::read_to_string(&file.path) {
                Ok(c) => c,
                Err(_) => continue,
            };

            if !content.contains("react-native") {
                continue;
            }

            for (idx, line) in content.lines().enumerate() {
                let trimmed = line.trim();
                if is_comment_line(trimmed) {
                    continue;
                }
                if trimmed.contains("style={{") {
                    findings.push(Finding {
                        id: String::new(),
                        rule_id: "framework.react-native.inline-style".to_string(),
        recommendation: Finding::recommendation_for_rule_id("framework.react-native.inline-style"),
                        title: "Inline style object in JSX".to_string(),
                        description: concat!(
                            "Inline style objects (`style={{ ... }}`) create a new object on every render, ",
                            "which defeats memoization in `React.memo` and `PureComponent` children. ",
                            "Extract styles into a `StyleSheet.create` call outside the component."
                        )
                        .to_string(),
                        category: FindingCategory::Framework,
                        severity: Severity::Medium,
                        confidence: Default::default(),
                        evidence: vec![Evidence {
                            path: file.path.clone(),
                            line_start: idx + 1,
                            line_end: None,
                            snippet: trimmed.to_string(),
                        }],
                        workspace_package: None,
                        docs_url: Some("https://reactnative.dev/docs/stylesheet".to_string()),
                    });
                    break;
                }
            }
        }

        findings
    }
}

const DEPRECATED_RN_APIS: &[(&str, &str)] = &[
    ("ViewPagerAndroid", "react-native-pager-view"),
    ("ToolbarAndroid", "@react-native-community/toolbar-android"),
    (
        "DatePickerAndroid",
        "@react-native-community/datetimepicker",
    ),
    (
        "TimePickerAndroid",
        "@react-native-community/datetimepicker",
    ),
    ("MaskedViewIOS", "@react-native-masked-view/masked-view"),
    (
        "ProgressBarAndroid",
        "@react-native-community/progress-bar-android",
    ),
    ("ProgressViewIOS", "@react-native-community/progress-view"),
    (
        "SegmentedControlIOS",
        "@react-native-community/segmented-control",
    ),
    ("CheckBox", "@react-native-community/checkbox"),
];

pub struct RnDeprecatedApiAudit;

impl ProjectAudit for RnDeprecatedApiAudit {
    fn audit(&self, facts: &ScanFacts, _config: &ScanConfig) -> Vec<Finding> {
        let mut findings = Vec::new();

        for file in &facts.files {
            if !is_js_file(&file.path) {
                continue;
            }

            let content = match std::fs::read_to_string(&file.path) {
                Ok(c) => c,
                Err(_) => continue,
            };

            if !content.contains("react-native") {
                continue;
            }

            for (idx, line) in content.lines().enumerate() {
                let trimmed = line.trim();
                if is_comment_line(trimmed) {
                    continue;
                }
                for (api, replacement) in DEPRECATED_RN_APIS {
                    if trimmed.contains(api) {
                        findings.push(Finding {
                            id: String::new(),
                            rule_id: "framework.react-native.deprecated-api".to_string(),
        recommendation: Finding::recommendation_for_rule_id("framework.react-native.deprecated-api"),
                            title: format!("Deprecated React Native API: {api}"),
                            description: format!(
                                "`{api}` was removed from React Native core. \
                                Replace it with `{replacement}` from the React Native community packages."
                            ),
                            category: FindingCategory::Framework,
                            severity: Severity::High,
                            confidence: Default::default(),
                            evidence: vec![Evidence {
                                path: file.path.clone(),
                                line_start: idx + 1,
                                line_end: None,
                                snippet: trimmed.to_string(),
                            }],
                            workspace_package: None,
                            docs_url: Some(
                                "https://reactnative.dev/docs/out-of-tree-platforms".to_string(),
                            ),
                        });
                        break;
                    }
                }
            }
        }

        findings
    }
}

pub struct RnFlatListMissingKeyAudit;

impl ProjectAudit for RnFlatListMissingKeyAudit {
    fn audit(&self, facts: &ScanFacts, _config: &ScanConfig) -> Vec<Finding> {
        let mut findings = Vec::new();

        for file in &facts.files {
            let ext = file.path.extension().and_then(|e| e.to_str()).unwrap_or("");
            if ext != "tsx" && ext != "jsx" {
                continue;
            }

            let content = match std::fs::read_to_string(&file.path) {
                Ok(c) => c,
                Err(_) => continue,
            };

            if !content.contains("react-native") {
                continue;
            }

            let lines: Vec<&str> = content.lines().collect();
            let mut i = 0;
            while i < lines.len() {
                let trimmed = lines[i].trim();
                if trimmed.contains("<FlatList") && !is_comment_line(trimmed) {
                    let window_end = (i + 20).min(lines.len());
                    let window: String = lines[i..window_end].join("\n");

                    if !window.contains("keyExtractor") {
                        findings.push(Finding {
                            id: String::new(),
                            rule_id: "framework.react-native.flatlist-missing-key".to_string(),
        recommendation: Finding::recommendation_for_rule_id("framework.react-native.flatlist-missing-key"),
                            title: "FlatList is missing keyExtractor".to_string(),
                            description: concat!(
                                "A `FlatList` without `keyExtractor` falls back to array index keys, ",
                                "which breaks list reconciliation when items are reordered or removed. ",
                                "Add `keyExtractor={(item) => item.id.toString()}` (or equivalent unique key)."
                            )
                            .to_string(),
                            category: FindingCategory::Framework,
                            severity: Severity::Low,
                            confidence: Default::default(),
                            evidence: vec![Evidence {
                                path: file.path.clone(),
                                line_start: i + 1,
                                line_end: None,
                                snippet: trimmed.to_string(),
                            }],
                            workspace_package: None,
                            docs_url: Some(
                                "https://reactnative.dev/docs/flatlist#keyextractor".to_string(),
                            ),
                        });
                    }
                    i = window_end;
                    continue;
                }
                i += 1;
            }
        }

        findings
    }
}