Skip to main content

cloudcover_javascript/
analysis.rs

1use std::{path::Path, process::Command};
2
3use cloudcover_core::{JavaScriptMethodReference, SdkModule};
4
5use crate::{JavaScriptAnalysisError, response::AnalyzerResponse};
6
7const ANALYZER: &str = include_str!("../analyzer/analyzer.js");
8
9#[derive(Clone, Debug, Eq, PartialEq)]
10pub struct JavaScriptAnalysis {
11    methods: Vec<JavaScriptMethodReference>,
12    modules: Vec<SdkModule>,
13}
14
15impl JavaScriptAnalysis {
16    #[must_use]
17    pub fn methods(&self) -> &[JavaScriptMethodReference] {
18        &self.methods
19    }
20
21    #[must_use]
22    pub fn modules(&self) -> &[SdkModule] {
23        &self.modules
24    }
25}
26
27/// Analyzes JavaScript or TypeScript source without executing the project or its dependencies.
28///
29/// The analyzed directory must have a locally resolvable `typescript` package. The embedded
30/// analyzer loads that compiler through Node's module resolver anchored at the directory.
31///
32/// # Errors
33///
34/// Returns [`JavaScriptAnalysisError`] when the directory is invalid, Node or TypeScript cannot
35/// be used, or the analyzer reports an actionable source-resolution failure.
36pub fn analyze_dir(path: impl AsRef<Path>) -> Result<JavaScriptAnalysis, JavaScriptAnalysisError> {
37    let path = path.as_ref();
38    if !path.is_dir() {
39        return Err(JavaScriptAnalysisError::NotDirectory {
40            path: path.to_path_buf(),
41        });
42    }
43
44    let path = path
45        .to_str()
46        .ok_or_else(|| JavaScriptAnalysisError::NonUtf8Path {
47            path: path.to_path_buf(),
48        })?;
49    let output = Command::new("node")
50        .arg("-e")
51        .arg(ANALYZER)
52        .arg(path)
53        .output()
54        .map_err(JavaScriptAnalysisError::NodeUnavailable)?;
55
56    if !output.status.success() {
57        return Err(JavaScriptAnalysisError::NodeFailed {
58            status: output.status,
59            stderr: String::from_utf8_lossy(&output.stderr).trim().to_owned(),
60        });
61    }
62
63    let response: AnalyzerResponse =
64        serde_json::from_slice(&output.stdout).map_err(JavaScriptAnalysisError::InvalidResponse)?;
65    let (methods, modules) = response
66        .into_analysis()
67        .map_err(JavaScriptAnalysisError::Analyzer)?;
68    Ok(JavaScriptAnalysis { methods, modules })
69}