Skip to main content

normalize_native_rules/
dead_parameter.rs

1//! `dead-parameter` native rule — flags function parameters never referenced in the function body.
2//!
3//! Uses tree-sitter `locals.scm` queries (via `normalize-scope`) to find parameters
4//! defined with `@local.definition.parameter` that have no resolved reference in the
5//! same file. Underscore-prefixed names (`_`, `_unused`) are excluded — those are the
6//! conventional way to mark intentionally unused parameters.
7//!
8//! # Language support
9//!
10//! Requires `@local.definition.parameter` captures in the language's `locals.scm`.
11//! Currently supported: Rust, Python, JavaScript, TypeScript, TSX, Go, Java, C, C++, C#.
12//! Languages without this capture are silently skipped.
13
14use normalize_languages::parsers::grammar_loader;
15use normalize_languages::support_for_path;
16use normalize_output::diagnostics::{DiagnosticsReport, Issue, Severity};
17use normalize_scope::ScopeEngine;
18use std::path::Path;
19
20use crate::cache::{FileRule, run_file_rule};
21use normalize_rules_config::WalkConfig;
22
23/// Serializable per-file finding for the dead-parameter rule.
24#[derive(serde::Serialize, serde::Deserialize)]
25pub struct DeadParameterFinding {
26    rel_path: String,
27    /// Name of the unused parameter.
28    name: String,
29    /// 1-based line number where the parameter is defined.
30    line: usize,
31}
32
33/// Rule that flags function parameters never referenced in their function body.
34pub struct DeadParameterRule;
35
36impl FileRule for DeadParameterRule {
37    type Finding = DeadParameterFinding;
38
39    fn engine_name(&self) -> &str {
40        "dead-parameter"
41    }
42
43    fn config_hash(&self) -> String {
44        // No configurable threshold; any change to the rule source invalidates the cache.
45        "1".into()
46    }
47
48    fn check_file(&self, path: &Path, root: &Path) -> Vec<Self::Finding> {
49        let support = match support_for_path(path) {
50            Some(s) => s,
51            None => return Vec::new(),
52        };
53        let content = match std::fs::read_to_string(path) {
54            Ok(c) => c,
55            Err(_) => return Vec::new(),
56        };
57
58        let loader = grammar_loader();
59        let engine = ScopeEngine::new(&loader);
60        let lang = support.grammar_name();
61
62        // Skip languages that have no locals.scm (engine returns empty for those).
63        if !engine.has_locals(lang) {
64            return Vec::new();
65        }
66
67        let unused = engine.find_unused_parameters(lang, &content);
68        if unused.is_empty() {
69            return Vec::new();
70        }
71
72        let rel_path = path
73            .strip_prefix(root)
74            .unwrap_or(path)
75            .to_string_lossy()
76            .to_string();
77
78        unused
79            .into_iter()
80            .map(|def| DeadParameterFinding {
81                rel_path: rel_path.clone(),
82                name: def.name,
83                line: def.location.line,
84            })
85            .collect()
86    }
87
88    fn to_diagnostics(
89        &self,
90        findings: Vec<(std::path::PathBuf, Vec<Self::Finding>)>,
91        _root: &Path,
92        files_checked: usize,
93    ) -> DiagnosticsReport {
94        let issues: Vec<Issue> = findings
95            .into_iter()
96            .flat_map(|(_path, file_findings)| file_findings)
97            .map(|f| Issue {
98                file: f.rel_path,
99                line: Some(f.line),
100                column: None,
101                end_line: None,
102                end_column: None,
103                rule_id: "dead-parameter".into(),
104                message: format!("parameter `{}` is never used", f.name),
105                severity: Severity::Warning,
106                source: "dead-parameter".into(),
107                related: vec![],
108                suggestion: Some(
109                    "prefix with `_` to mark it intentionally unused, or remove it if possible"
110                        .into(),
111                ),
112            })
113            .collect();
114
115        DiagnosticsReport {
116            issues,
117            files_checked,
118            sources_run: vec!["dead-parameter".into()],
119            tool_errors: vec![],
120            daemon_cached: false,
121        }
122    }
123}
124
125/// Build a `DiagnosticsReport` for the `dead-parameter` rule.
126///
127/// Walks all source files under `root`, analyzes each with the scope engine, and emits
128/// a warning for every function parameter that is never referenced in the file.
129pub fn build_dead_parameter_report(
130    root: &Path,
131    explicit_files: Option<&[std::path::PathBuf]>,
132    walk_config: &WalkConfig,
133) -> DiagnosticsReport {
134    let rule = DeadParameterRule;
135    run_file_rule(&rule, root, explicit_files, walk_config)
136}