use std::path::Path;
use self::ConfigMatch::{AllOf, Exact, Extension, Prefix};
use super::files::ProjectFiles;
use super::python;
use crate::StackLabel;
use crate::ecosystem::Ecosystem as E;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ConfigMatch {
Exact(&'static str),
Prefix(&'static str),
AllOf(&'static [&'static str]),
Extension(&'static str),
}
pub type ConfigRule = (ConfigMatch, StackLabel, E);
pub const CONFIG_RULES: &[ConfigRule] = &[
(
Prefix("next.config"),
StackLabel::framework("Next.js"),
E::Node,
),
(
Prefix("nuxt.config"),
StackLabel::framework("Nuxt"),
E::Node,
),
(
Exact("angular.json"),
StackLabel::framework("Angular"),
E::Node,
),
(
Prefix("svelte.config"),
StackLabel::framework("SvelteKit"),
E::Node,
),
(
Prefix("astro.config"),
StackLabel::framework("Astro"),
E::Node,
),
(Prefix("vite.config"), StackLabel::tool("Vite"), E::Node),
(
Prefix("remix.config"),
StackLabel::framework("Remix"),
E::Node,
),
(
Prefix("gatsby-config"),
StackLabel::framework("Gatsby"),
E::Node,
),
(Prefix("vue.config"), StackLabel::tool("Vue CLI"), E::Node),
(
Prefix("webpack.config"),
StackLabel::tool("Webpack"),
E::Node,
),
(Exact("Cargo.toml"), StackLabel::runtime("Rust"), E::Rust),
(Exact("go.mod"), StackLabel::runtime("Go"), E::Go),
(Exact("go.work"), StackLabel::runtime("Go"), E::Go),
(Exact("pom.xml"), StackLabel::tool("Java (Maven)"), E::Jvm),
(
Exact("build.gradle.kts"),
StackLabel::tool("Kotlin (Gradle)"),
E::Jvm,
),
(
Exact("build.gradle"),
StackLabel::tool("Java (Gradle)"),
E::Jvm,
),
(
AllOf(&["artisan", "composer.json"]),
StackLabel::framework("Laravel"),
E::Php,
),
(Exact("composer.json"), StackLabel::runtime("PHP"), E::Php),
(Exact("mix.exs"), StackLabel::runtime("Elixir"), E::Beam),
(Exact("deno.json"), StackLabel::runtime("Deno"), E::Deno),
(Exact("deno.jsonc"), StackLabel::runtime("Deno"), E::Deno),
];
pub const LATE_CONFIG_RULES: &[ConfigRule] = &[
(
AllOf(&["Gemfile", "config.ru", "bin/rails"]),
StackLabel::framework("Rails"),
E::Ruby,
),
(AllOf(&["Gemfile", "config.ru"]), RACK_LABEL, E::Ruby),
(Extension("csproj"), StackLabel::runtime(".NET"), E::DotNet),
(
Extension("fsproj"),
StackLabel::runtime(".NET (F#)"),
E::DotNet,
),
];
pub const RACK_LABEL: StackLabel = StackLabel::framework("Ruby (Rack)");
const COMMON_CONFIG_SUFFIXES: &[&str] = &["", ".js", ".cjs", ".mjs", ".ts", ".cts", ".mts"];
#[must_use]
pub fn detect_from_config(project_root: &Path) -> Option<StackLabel> {
detect_for_ecosystem(project_root, None)
}
pub fn detect_for_ecosystem(project_root: &Path, scope: Option<E>) -> Option<StackLabel> {
let files = ProjectFiles::read(project_root)?;
let in_scope = |ecosystem: E| scope.is_none_or(|scope| scope.accepts_config(ecosystem));
let python_process = scope == Some(E::Python);
detect_from_rules(project_root, &files, CONFIG_RULES, in_scope)
.or_else(|| {
in_scope(E::Python)
.then(|| python::detect_python_project(project_root, &files, python_process))
.flatten()
})
.or_else(|| detect_from_rules(project_root, &files, LATE_CONFIG_RULES, in_scope))
}
fn detect_from_rules(
project_root: &Path,
files: &ProjectFiles,
rules: &[ConfigRule],
in_scope: impl Fn(E) -> bool,
) -> Option<StackLabel> {
rules
.iter()
.filter(|(_, _, ecosystem)| in_scope(*ecosystem))
.find(|(matcher, _, _)| rule_matches(project_root, files, *matcher))
.map(|(_, label, _)| label.clone())
}
fn rule_matches(project_root: &Path, files: &ProjectFiles, matcher: ConfigMatch) -> bool {
match matcher {
Exact(name) => files.contains_exact(name),
Prefix(prefix) => files.contains_prefix(prefix),
AllOf(paths) => paths
.iter()
.all(|path| files.contains_path(project_root, path)),
Extension(extension) => files.contains_extension(extension),
}
}
pub(super) fn matches_config_name_prefix(name: &str, pattern: &str) -> bool {
name.strip_prefix(pattern)
.is_some_and(|suffix| COMMON_CONFIG_SUFFIXES.contains(&suffix))
}