1use std::fs;
2use std::path::{Path, PathBuf};
3
4use pedant_types::{AnalysisTier, ExecutionContext};
5
6use crate::analysis_result::AnalysisResult;
7use crate::capabilities::detect_capabilities;
8use crate::check_config::CheckConfig;
9use crate::ir;
10use crate::ir::DataFlowFact;
11use crate::ir::extract::compute_fingerprints;
12use crate::ir::semantic::SemanticContext;
13use crate::style::check_style;
14
15type ManifestPresence = (bool, bool);
16
17#[derive(Debug, thiserror::Error)]
19pub enum LintError {
20 #[error("IO error: {0}")]
22 IoError(#[from] std::io::Error),
23 #[error("parse error: {0}")]
25 ParseError(#[from] syn::Error),
26 #[error("TOML parse error: {0}")]
28 TomlParseError(#[from] toml::de::Error),
29}
30
31pub fn analyze(
36 file_path: &str,
37 source: &str,
38 config: &CheckConfig,
39 semantic: Option<&SemanticContext>,
40) -> Result<AnalysisResult, syn::Error> {
41 analyze_inner(file_path, source, config, semantic, None)
42}
43
44pub fn analyze_build_script(
46 file_path: &str,
47 source: &str,
48 config: &CheckConfig,
49 semantic: Option<&SemanticContext>,
50) -> Result<AnalysisResult, syn::Error> {
51 analyze_inner(
52 file_path,
53 source,
54 config,
55 semantic,
56 Some(ExecutionContext::BuildHook),
57 )
58}
59
60fn analyze_inner(
61 file_path: &str,
62 source: &str,
63 config: &CheckConfig,
64 semantic: Option<&SemanticContext>,
65 execution_context: Option<ExecutionContext>,
66) -> Result<AnalysisResult, syn::Error> {
67 let syntax = syn::parse_file(source)?;
68 let mut ir = ir::extract(file_path, &syntax, semantic);
69 ir.source_line_count = source.lines().count();
70 let violations = check_style(&ir, config).into_boxed_slice();
71 let capabilities = detect_capabilities(&ir, execution_context);
72
73 #[cfg(feature = "semantic")]
74 let capabilities = {
75 let mut caps = capabilities;
76 if let Some(ctx) = semantic {
77 enrich_reachability(&mut caps.findings, ctx);
78 }
79 caps
80 };
81
82 let fn_fingerprints = compute_fingerprints(&ir);
83
84 Ok(AnalysisResult {
85 violations,
86 capabilities,
87 data_flows: ir.data_flows,
88 fn_fingerprints,
89 })
90}
91
92pub fn lint_str(source: &str, config: &CheckConfig) -> Result<AnalysisResult, LintError> {
94 analyze("<string>", source, config, None).map_err(LintError::from)
95}
96
97pub fn lint_file(path: &Path, config: &CheckConfig) -> Result<AnalysisResult, LintError> {
99 let source = fs::read_to_string(path)?;
100 let file_path = path.to_string_lossy();
101 analyze(&file_path, &source, config, None).map_err(LintError::from)
102}
103
104pub fn discover_workspace_root(start: &Path) -> Result<Option<PathBuf>, LintError> {
110 let start_dir = match (start.is_dir(), start.parent()) {
111 (true, _) => start,
112 (false, Some(parent)) => parent,
113 (false, None) => return Ok(None),
114 };
115
116 let mut nearest_package: Option<PathBuf> = None;
117 for dir in start_dir.ancestors() {
118 let cargo_toml = dir.join("Cargo.toml");
119 let (has_workspace, has_package) = read_manifest_presence(&cargo_toml)?;
120 match (has_workspace, has_package, nearest_package.is_some()) {
121 (true, _, _) => return Ok(Some(dir.to_path_buf())),
122 (false, true, false) => nearest_package = Some(dir.to_path_buf()),
123 _ => {}
124 }
125 }
126 Ok(nearest_package)
127}
128
129fn read_manifest_presence(cargo_toml: &Path) -> Result<ManifestPresence, LintError> {
130 let contents = match fs::read_to_string(cargo_toml) {
131 Ok(contents) => contents,
132 Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok((false, false)),
133 Err(error) => return Err(LintError::IoError(error)),
134 };
135 let table: toml::Table = contents.parse()?;
136 Ok((
137 contains_manifest_table(&table, "workspace"),
138 contains_manifest_table(&table, "package"),
139 ))
140}
141
142fn contains_manifest_table(table: &toml::Table, section_name: &str) -> bool {
143 table
144 .get(section_name)
145 .and_then(toml::Value::as_table)
146 .is_some()
147}
148
149pub fn discover_build_script(crate_root: &Path) -> Result<Option<PathBuf>, LintError> {
154 let cargo_toml_path = crate_root.join("Cargo.toml");
155 let cargo_toml_contents = match fs::read_to_string(&cargo_toml_path) {
156 Ok(contents) => contents,
157 Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(None),
158 Err(e) => return Err(LintError::IoError(e)),
159 };
160 let table: toml::Table = cargo_toml_contents.parse()?;
161
162 let custom_path = table
163 .get("package")
164 .and_then(toml::Value::as_table)
165 .and_then(|pkg| pkg.get("build"))
166 .and_then(toml::Value::as_str);
167
168 let candidate = match custom_path {
169 Some(build_path) => crate_root.join(build_path),
170 None => crate_root.join("build.rs"),
171 };
172
173 Ok(candidate.is_file().then_some(candidate))
174}
175
176pub fn analyze_with_build_script(
181 file_path: &str,
182 source: &str,
183 config: &CheckConfig,
184 semantic: Option<&SemanticContext>,
185 build_source: Option<(&str, &str)>,
186) -> Result<AnalysisResult, syn::Error> {
187 let mut result = analyze(file_path, source, config, semantic)?;
188
189 let Some((build_path, build_src)) = build_source else {
190 return Ok(result);
191 };
192
193 let build_caps = analyze_build_script(build_path, build_src, config, semantic)?.capabilities;
194
195 let mut merged = result.capabilities.findings.into_vec();
196 merged.extend(build_caps.findings);
197 result.capabilities.findings = merged.into_boxed_slice();
198
199 Ok(result)
200}
201
202pub fn determine_analysis_tier(
209 semantic: Option<&SemanticContext>,
210 data_flows: &[DataFlowFact],
211) -> AnalysisTier {
212 match (semantic.is_some(), !data_flows.is_empty()) {
213 (true, true) => AnalysisTier::DataFlow,
214 (true, false) => AnalysisTier::Semantic,
215 (false, _) => AnalysisTier::Syntactic,
216 }
217}
218
219#[cfg(feature = "semantic")]
225fn enrich_reachability(findings: &mut [pedant_types::CapabilityFinding], ctx: &SemanticContext) {
226 use std::collections::BTreeMap;
227 use std::sync::Arc;
228
229 let mut by_file: BTreeMap<Arc<str>, Vec<usize>> = BTreeMap::new();
231 for (idx, finding) in findings.iter().enumerate() {
232 by_file
233 .entry(Arc::clone(&finding.location.file))
234 .or_default()
235 .push(idx);
236 }
237
238 for (file, indices) in &by_file {
239 let Some(analysis) = ctx.analyze_file(file) else {
240 continue;
241 };
242 let lines: Vec<usize> = indices.iter().map(|&i| findings[i].location.line).collect();
243 let results = analysis.check_reachability_batch(&lines);
244 for (pos, &idx) in indices.iter().enumerate() {
245 findings[idx].reachable = Some(results[pos]);
246 }
247 }
248}