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::project::{FileShape, project_shape};
14use crate::style::check_style;
15
16type ManifestPresence = (bool, bool);
17
18#[derive(Debug, thiserror::Error)]
20pub enum LintError {
21 #[error("IO error: {0}")]
23 IoError(#[from] std::io::Error),
24 #[error("parse error: {0}")]
26 ParseError(#[from] syn::Error),
27 #[error("TOML parse error: {0}")]
29 TomlParseError(#[from] toml::de::Error),
30}
31
32pub fn analyze(
37 file_path: &str,
38 source: &str,
39 config: &CheckConfig,
40 semantic: Option<&SemanticContext>,
41) -> Result<AnalysisResult, syn::Error> {
42 Ok(analyze_inner(file_path, source, config, semantic, None)?.0)
43}
44
45pub fn analyze_build_script(
47 file_path: &str,
48 source: &str,
49 config: &CheckConfig,
50 semantic: Option<&SemanticContext>,
51) -> Result<AnalysisResult, syn::Error> {
52 Ok(analyze_inner(
53 file_path,
54 source,
55 config,
56 semantic,
57 Some(ExecutionContext::BuildHook),
58 )?
59 .0)
60}
61
62pub fn analyze_with_shape(
68 file_path: &str,
69 source: &str,
70 config: &CheckConfig,
71 semantic: Option<&SemanticContext>,
72) -> Result<(AnalysisResult, FileShape), syn::Error> {
73 analyze_inner(file_path, source, config, semantic, None)
74}
75
76pub fn analyze_build_script_with_shape(
78 file_path: &str,
79 source: &str,
80 config: &CheckConfig,
81 semantic: Option<&SemanticContext>,
82) -> Result<(AnalysisResult, FileShape), syn::Error> {
83 analyze_inner(
84 file_path,
85 source,
86 config,
87 semantic,
88 Some(ExecutionContext::BuildHook),
89 )
90}
91
92fn analyze_inner(
93 file_path: &str,
94 source: &str,
95 config: &CheckConfig,
96 semantic: Option<&SemanticContext>,
97 execution_context: Option<ExecutionContext>,
98) -> Result<(AnalysisResult, FileShape), syn::Error> {
99 let syntax = syn::parse_file(source)?;
100 let mut ir = ir::extract(file_path, &syntax, semantic);
101 ir.source_line_count = source.lines().count();
102 let violations = check_style(&ir, config).into_boxed_slice();
103 let shape = project_shape(&ir, config);
104 let capabilities = detect_capabilities(&ir, execution_context);
105
106 #[cfg(feature = "semantic")]
107 let capabilities = {
108 let mut caps = capabilities;
109 if let Some(ctx) = semantic {
110 enrich_reachability(&mut caps.findings, ctx);
111 }
112 caps
113 };
114
115 let fn_fingerprints = compute_fingerprints(&ir);
116
117 Ok((
118 AnalysisResult {
119 violations,
120 capabilities,
121 data_flows: ir.data_flows,
122 fn_fingerprints,
123 },
124 shape,
125 ))
126}
127
128pub fn lint_str(source: &str, config: &CheckConfig) -> Result<AnalysisResult, LintError> {
130 analyze("<string>", source, config, None).map_err(LintError::from)
131}
132
133pub fn lint_file(path: &Path, config: &CheckConfig) -> Result<AnalysisResult, LintError> {
135 let source = fs::read_to_string(path)?;
136 let file_path = path.to_string_lossy();
137 analyze(&file_path, &source, config, None).map_err(LintError::from)
138}
139
140pub fn discover_workspace_root(start: &Path) -> Result<Option<PathBuf>, LintError> {
146 let start_dir = match (start.is_dir(), start.parent()) {
147 (true, _) => start,
148 (false, Some(parent)) => parent,
149 (false, None) => return Ok(None),
150 };
151
152 let mut nearest_package: Option<PathBuf> = None;
153 for dir in start_dir.ancestors() {
154 let cargo_toml = dir.join("Cargo.toml");
155 let (has_workspace, has_package) = read_manifest_presence(&cargo_toml)?;
156 match (has_workspace, has_package, nearest_package.is_some()) {
157 (true, _, _) => return Ok(Some(dir.to_path_buf())),
158 (false, true, false) => nearest_package = Some(dir.to_path_buf()),
159 _ => {}
160 }
161 }
162 Ok(nearest_package)
163}
164
165pub fn discover_crate_root(file: &Path) -> Option<&Path> {
171 let mut dir = file.parent()?;
172 loop {
173 match dir.join("Cargo.toml").is_file() {
174 true => return Some(dir),
175 false => dir = dir.parent()?,
176 }
177 }
178}
179
180fn read_manifest_presence(cargo_toml: &Path) -> Result<ManifestPresence, LintError> {
181 let contents = match fs::read_to_string(cargo_toml) {
182 Ok(contents) => contents,
183 Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok((false, false)),
184 Err(error) => return Err(LintError::IoError(error)),
185 };
186 let table: toml::Table = contents.parse()?;
187 Ok((
188 contains_manifest_table(&table, "workspace"),
189 contains_manifest_table(&table, "package"),
190 ))
191}
192
193fn contains_manifest_table(table: &toml::Table, section_name: &str) -> bool {
194 table
195 .get(section_name)
196 .and_then(toml::Value::as_table)
197 .is_some()
198}
199
200pub fn discover_build_script(crate_root: &Path) -> Result<Option<PathBuf>, LintError> {
205 let cargo_toml_path = crate_root.join("Cargo.toml");
206 let cargo_toml_contents = match fs::read_to_string(&cargo_toml_path) {
207 Ok(contents) => contents,
208 Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(None),
209 Err(e) => return Err(LintError::IoError(e)),
210 };
211 let table: toml::Table = cargo_toml_contents.parse()?;
212
213 let custom_path = table
214 .get("package")
215 .and_then(toml::Value::as_table)
216 .and_then(|pkg| pkg.get("build"))
217 .and_then(toml::Value::as_str);
218
219 let candidate = match custom_path {
220 Some(build_path) => crate_root.join(build_path),
221 None => crate_root.join("build.rs"),
222 };
223
224 Ok(candidate.is_file().then_some(candidate))
225}
226
227pub fn analyze_with_build_script(
232 file_path: &str,
233 source: &str,
234 config: &CheckConfig,
235 semantic: Option<&SemanticContext>,
236 build_source: Option<(&str, &str)>,
237) -> Result<AnalysisResult, syn::Error> {
238 let mut result = analyze(file_path, source, config, semantic)?;
239
240 let Some((build_path, build_src)) = build_source else {
241 return Ok(result);
242 };
243
244 let build_caps = analyze_build_script(build_path, build_src, config, semantic)?.capabilities;
245
246 let mut merged = result.capabilities.findings.into_vec();
247 merged.extend(build_caps.findings);
248 result.capabilities.findings = merged.into_boxed_slice();
249
250 Ok(result)
251}
252
253pub fn determine_analysis_tier(
260 semantic: Option<&SemanticContext>,
261 data_flows: &[DataFlowFact],
262) -> AnalysisTier {
263 match (semantic.is_some(), !data_flows.is_empty()) {
264 (true, true) => AnalysisTier::DataFlow,
265 (true, false) => AnalysisTier::Semantic,
266 (false, _) => AnalysisTier::Syntactic,
267 }
268}
269
270#[cfg(feature = "semantic")]
276fn enrich_reachability(findings: &mut [pedant_types::CapabilityFinding], ctx: &SemanticContext) {
277 use std::collections::BTreeMap;
278 use std::sync::Arc;
279
280 let mut by_file: BTreeMap<Arc<str>, Vec<usize>> = BTreeMap::new();
282 for (idx, finding) in findings.iter().enumerate() {
283 by_file
284 .entry(Arc::clone(&finding.location.file))
285 .or_default()
286 .push(idx);
287 }
288
289 for (file, indices) in &by_file {
290 let Some(analysis) = ctx.analyze_file(file) else {
291 continue;
292 };
293 let lines: Vec<usize> = indices.iter().map(|&i| findings[i].location.line).collect();
294 let results = analysis.check_reachability_batch(&lines);
295 for (pos, &idx) in indices.iter().enumerate() {
296 findings[idx].reachable = Some(results[pos]);
297 }
298 }
299}