Skip to main content

normalize_manifest/
lib.rs

1//! Manifest file parsing for programming language ecosystems.
2//!
3//! Provides a uniform `ParsedManifest` type and parsers for common package
4//! manifest formats.  See `docs/manifest-support.md` for full coverage status.
5//!
6//! ## Dispatch
7//!
8//! - `parse_manifest(filename, content)` — dispatches by exact filename
9//! - `parse_manifest_by_extension(ext, content)` — for wildcard-named files
10//!   (`.nimble`, `.cabal`, `.csproj`, `.rockspec`)
11//!
12//! ## Convenience helpers
13//!
14//! - `go_module(content)` — extract module info from `go.mod`
15//! - `npm_entry_point(content)` — extract entry point from `package.json`
16
17#[cfg(feature = "eval")]
18pub mod eval;
19
20pub mod cabal;
21pub mod cabal_project;
22pub mod cargo;
23pub mod clojure;
24pub mod common_lisp;
25pub mod composer;
26pub mod conan;
27pub mod crystal;
28pub mod dub;
29pub mod dune;
30pub mod elm;
31pub mod erlang;
32pub mod flake;
33pub mod fortran_fpm;
34pub mod gemfile;
35pub mod gleam;
36pub mod go_mod;
37pub mod gradle;
38pub mod gradle_libs;
39pub mod julia;
40pub mod maven;
41pub mod mix_exs;
42pub mod nimble;
43pub mod npm;
44pub mod nuget;
45pub mod ocaml;
46pub mod perl;
47pub mod pip;
48pub mod pipfile;
49pub mod pubspec;
50pub mod purescript;
51pub mod pyproject;
52pub mod r_description;
53pub mod racket;
54pub mod rockspec;
55pub mod sbt;
56pub mod setup_cfg;
57pub mod setup_py;
58pub mod sexpr;
59pub mod stack;
60pub mod swift_pm;
61pub mod vcpkg;
62pub mod vlang;
63pub mod zig;
64
65pub use go_mod::GoModule;
66pub use npm::npm_entry_point;
67
68use serde::Serialize;
69
70// ============================================================================
71// Core types
72// ============================================================================
73
74/// The kind of dependency relationship.
75#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
76#[serde(rename_all = "lowercase")]
77pub enum DepKind {
78    Normal,
79    Dev,
80    Build,
81    Optional,
82}
83
84/// A declared dependency extracted from a manifest.
85#[derive(Debug, Clone, Serialize)]
86pub struct DeclaredDep {
87    pub name: String,
88    /// Version requirement string (e.g., `"^1.0"`, `">=2"`, `"v0.9.1"`).
89    pub version_req: Option<String>,
90    pub kind: DepKind,
91}
92
93/// Parsed contents of a project manifest file.
94#[derive(Debug, Clone, Serialize)]
95pub struct ParsedManifest {
96    /// Ecosystem identifier: `"cargo"`, `"go"`, `"npm"`, `"pip"`, `"python"`,
97    /// `"composer"`, `"maven"`, `"gradle"`, `"sbt"`, `"hex"`, `"pub"`,
98    /// `"bundler"`, `"conan"`, `"dub"`, `"nimble"`, `"cabal"`, `"luarocks"`,
99    /// `"stackage"`, `"spm"`, `"nix"`, `"nuget"`.
100    pub ecosystem: &'static str,
101    pub name: Option<String>,
102    pub version: Option<String>,
103    pub dependencies: Vec<DeclaredDep>,
104}
105
106// ============================================================================
107// ManifestParser trait
108// ============================================================================
109
110/// Error returned by manifest parsers.
111#[derive(Debug)]
112pub struct ManifestError(pub String);
113
114impl std::fmt::Display for ManifestError {
115    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
116        write!(f, "{}", self.0)
117    }
118}
119
120impl std::error::Error for ManifestError {}
121
122/// A parser for a specific manifest file format.
123pub trait ManifestParser: Send + Sync {
124    /// The canonical filename this parser handles (e.g., `"Cargo.toml"`).
125    /// Extension-based parsers use a glob pattern like `"*.nimble"`.
126    fn filename(&self) -> &'static str;
127
128    /// Parse manifest content and return structured data.
129    fn parse(&self, content: &str) -> Result<ParsedManifest, ManifestError>;
130}
131
132// ============================================================================
133// Top-level convenience functions
134// ============================================================================
135
136/// Parse a manifest file by exact filename, dispatching to the correct parser.
137///
138/// Returns `None` if the filename is not recognized. For extension-based formats
139/// (`.nimble`, `.cabal`, `.csproj`, `.rockspec`), use `parse_manifest_by_extension`.
140pub fn parse_manifest(filename: &str, content: &str) -> Option<ParsedManifest> {
141    match filename {
142        // Rust
143        "Cargo.toml" => cargo::CargoParser.parse(content).ok(),
144        // Go
145        "go.mod" => go_mod::GoModParser.parse(content).ok(),
146        // Node / npm
147        "package.json" => npm::NpmParser.parse(content).ok(),
148        // Python
149        "requirements.txt" => pip::PipParser.parse(content).ok(),
150        "Pipfile" => pipfile::PipfileParser.parse(content).ok(),
151        "pyproject.toml" => pyproject::PyprojectParser.parse(content).ok(),
152        "setup.cfg" => setup_cfg::SetupCfgParser.parse(content).ok(),
153        "setup.py" => setup_py::SetupPyParser.parse(content).ok(),
154        // PHP
155        "composer.json" => composer::ComposerParser.parse(content).ok(),
156        // JVM
157        "pom.xml" => maven::MavenParser.parse(content).ok(),
158        "build.gradle" => gradle::GradleParser.parse(content).ok(),
159        "build.gradle.kts" => gradle::GradleKtsParser.parse(content).ok(),
160        "build.sbt" => sbt::SbtParser.parse(content).ok(),
161        // Elixir
162        "mix.exs" => mix_exs::MixExsParser.parse(content).ok(),
163        // Ruby
164        "Gemfile" => gemfile::GemfileParser.parse(content).ok(),
165        // Dart/Flutter
166        "pubspec.yaml" => pubspec::PubspecParser.parse(content).ok(),
167        // C/C++ (Conan)
168        "conanfile.txt" => conan::ConanTxtParser.parse(content).ok(),
169        "conanfile.py" => conan::ConanPyParser.parse(content).ok(),
170        // .NET/NuGet
171        "packages.config" => nuget::PackagesConfigParser.parse(content).ok(),
172        "Directory.Packages.props" => nuget::DirectoryPackagesPropsParser.parse(content).ok(),
173        // D language
174        "dub.json" => dub::DubJsonParser.parse(content).ok(),
175        "dub.sdl" => dub::DubSdlParser.parse(content).ok(),
176        // Haskell
177        "stack.yaml" => stack::StackParser.parse(content).ok(),
178        // Nix
179        "flake.nix" => flake::FlakeParser.parse(content).ok(),
180        // Swift
181        "Package.swift" => swift_pm::SwiftPmParser.parse(content).ok(),
182        // Gradle version catalog
183        "libs.versions.toml" => gradle_libs::GradleLibsParser.parse(content).ok(),
184        // vcpkg (C/C++)
185        "vcpkg.json" => vcpkg::VcpkgParser.parse(content).ok(),
186        // Elm
187        "elm.json" => elm::ElmParser.parse(content).ok(),
188        // Gleam
189        "gleam.toml" => gleam::GleamParser.parse(content).ok(),
190        // Julia
191        "Project.toml" => julia::JuliaParser.parse(content).ok(),
192        // Fortran Package Manager
193        "fpm.toml" => fortran_fpm::FortranFpmParser.parse(content).ok(),
194        // Clojure
195        "project.clj" => clojure::LeinParser.parse(content).ok(),
196        "deps.edn" => clojure::EclojureParser.parse(content).ok(),
197        // Crystal
198        "shard.yml" => crystal::CrystalShardsParser.parse(content).ok(),
199        // R
200        "DESCRIPTION" => r_description::RDescriptionParser.parse(content).ok(),
201        // Erlang
202        "rebar.config" => erlang::RebarConfigParser.parse(content).ok(),
203        // Perl
204        "cpanfile" => perl::CpanfileParser.parse(content).ok(),
205        // OCaml/Dune
206        "dune-project" => dune::DuneParser.parse(content).ok(),
207        // Zig
208        "build.zig.zon" => zig::ZigZonParser.parse(content).ok(),
209        // PureScript
210        "spago.yaml" => purescript::SpagoParser.parse(content).ok(),
211        // Racket
212        "info.rkt" => racket::RacketInfoParser.parse(content).ok(),
213        // V language
214        "v.mod" => vlang::VModParser.parse(content).ok(),
215        // Haskell Cabal project
216        "cabal.project" => cabal_project::CabalProjectParser.parse(content).ok(),
217        // Extension-based dispatch (wildcard filenames)
218        _ => parse_manifest_by_extension_impl(filename, content),
219    }
220}
221
222/// Parse a manifest file whose format is identified by file extension.
223///
224/// Handles: `.nimble`, `.cabal`, `.csproj`, `.rockspec`.
225///
226/// `filename` is the full filename (e.g. `"mypkg.nimble"`) or just the
227/// extension (e.g. `"nimble"`). Either form is accepted.
228pub fn parse_manifest_by_extension(filename: &str, content: &str) -> Option<ParsedManifest> {
229    parse_manifest_by_extension_impl(filename, content)
230}
231
232fn parse_manifest_by_extension_impl(filename: &str, content: &str) -> Option<ParsedManifest> {
233    let ext = filename.rsplit('.').next().unwrap_or(filename);
234
235    match ext {
236        "nimble" => nimble::NimbleParser.parse(content).ok(),
237        "cabal" => cabal::CabalParser.parse(content).ok(),
238        "csproj" | "vbproj" | "fsproj" => nuget::CsprojParser.parse(content).ok(),
239        "rockspec" => rockspec::RockspecParser.parse(content).ok(),
240        "opam" => ocaml::OpamParser.parse(content).ok(),
241        "asd" => common_lisp::AsdParser.parse(content).ok(),
242        _ => None,
243    }
244}
245
246// ============================================================================
247// Eval-backed parsing (feature = "eval")
248// ============================================================================
249
250/// Controls what happens when the runtime needed for eval is not available.
251#[cfg(feature = "eval")]
252#[derive(Debug, Clone, Copy, PartialEq, Eq)]
253pub enum EvalPolicy {
254    /// Try eval; silently fall back to the heuristic parser if the runtime is
255    /// absent or the command fails.
256    IfAvailable,
257    /// Return `None` if eval fails instead of falling back to heuristics.
258    Required,
259}
260
261/// Parse a manifest file, preferring runtime evaluation over heuristics.
262///
263/// Dispatches to an eval-backed parser when the language runtime is available
264/// (`swift`, `go`, `ruby`/`bundle`, `elixir`/`mix`). On failure or when the
265/// runtime is absent, falls back to `parse_manifest` unless `policy` is
266/// `EvalPolicy::Required`.
267///
268/// # Supported eval targets
269///
270/// | File | Command |
271/// |------|---------|
272/// | `Package.swift` | `swift package dump-package` |
273/// | `go.mod` | `go mod edit -json` |
274/// | `Gemfile` | `bundle exec ruby -e '…'` |
275/// | `mix.exs` | `elixir -e '…'` |
276///
277/// All other filenames fall through to `parse_manifest` immediately.
278#[cfg(feature = "eval")]
279pub fn parse_manifest_eval(
280    filename: &str,
281    content: &str,
282    root: &std::path::Path,
283    policy: EvalPolicy,
284) -> Option<ParsedManifest> {
285    match eval::try_eval(filename, root) {
286        Some(m) => Some(m),
287        None if policy == EvalPolicy::IfAvailable => parse_manifest(filename, content),
288        None => None,
289    }
290}
291
292/// Parse go.mod content to extract module information.
293///
294/// Convenience wrapper for `normalize-local-deps` internal use.
295pub fn go_module(content: &str) -> Option<GoModule> {
296    go_mod::parse_go_module(content)
297}