1use std::path::Path;
2
3use self::ConfigMatch::{AllOf, Exact, Extension, Prefix};
4use super::files::ProjectFiles;
5use super::python;
6use crate::StackLabel;
7use crate::ecosystem::Ecosystem as E;
8
9#[derive(Clone, Copy, Debug, PartialEq, Eq)]
11pub enum ConfigMatch {
12 Exact(&'static str),
14 Prefix(&'static str),
17 AllOf(&'static [&'static str]),
19 Extension(&'static str),
21}
22
23pub type ConfigRule = (ConfigMatch, StackLabel, E);
25
26pub const CONFIG_RULES: &[ConfigRule] = &[
28 (
29 Prefix("next.config"),
30 StackLabel::framework("Next.js"),
31 E::Node,
32 ),
33 (
34 Prefix("nuxt.config"),
35 StackLabel::framework("Nuxt"),
36 E::Node,
37 ),
38 (
39 Exact("angular.json"),
40 StackLabel::framework("Angular"),
41 E::Node,
42 ),
43 (
44 Prefix("svelte.config"),
45 StackLabel::framework("SvelteKit"),
46 E::Node,
47 ),
48 (
49 Prefix("astro.config"),
50 StackLabel::framework("Astro"),
51 E::Node,
52 ),
53 (Prefix("vite.config"), StackLabel::tool("Vite"), E::Node),
54 (
55 Prefix("remix.config"),
56 StackLabel::framework("Remix"),
57 E::Node,
58 ),
59 (
60 Prefix("gatsby-config"),
61 StackLabel::framework("Gatsby"),
62 E::Node,
63 ),
64 (Prefix("vue.config"), StackLabel::tool("Vue CLI"), E::Node),
65 (
66 Prefix("webpack.config"),
67 StackLabel::tool("Webpack"),
68 E::Node,
69 ),
70 (Exact("Cargo.toml"), StackLabel::runtime("Rust"), E::Rust),
71 (Exact("go.mod"), StackLabel::runtime("Go"), E::Go),
72 (Exact("go.work"), StackLabel::runtime("Go"), E::Go),
73 (Exact("pom.xml"), StackLabel::tool("Java (Maven)"), E::Jvm),
74 (
75 Exact("build.gradle.kts"),
76 StackLabel::tool("Kotlin (Gradle)"),
77 E::Jvm,
78 ),
79 (
80 Exact("build.gradle"),
81 StackLabel::tool("Java (Gradle)"),
82 E::Jvm,
83 ),
84 (
85 AllOf(&["artisan", "composer.json"]),
86 StackLabel::framework("Laravel"),
87 E::Php,
88 ),
89 (Exact("composer.json"), StackLabel::runtime("PHP"), E::Php),
90 (Exact("mix.exs"), StackLabel::runtime("Elixir"), E::Beam),
91 (Exact("deno.json"), StackLabel::runtime("Deno"), E::Deno),
92 (Exact("deno.jsonc"), StackLabel::runtime("Deno"), E::Deno),
93];
94
95pub const LATE_CONFIG_RULES: &[ConfigRule] = &[
97 (
98 AllOf(&["Gemfile", "config.ru", "bin/rails"]),
99 StackLabel::framework("Rails"),
100 E::Ruby,
101 ),
102 (AllOf(&["Gemfile", "config.ru"]), RACK_LABEL, E::Ruby),
103 (Extension("csproj"), StackLabel::runtime(".NET"), E::DotNet),
104 (
105 Extension("fsproj"),
106 StackLabel::runtime(".NET (F#)"),
107 E::DotNet,
108 ),
109];
110
111pub const RACK_LABEL: StackLabel = StackLabel::framework("Ruby (Rack)");
114
115const COMMON_CONFIG_SUFFIXES: &[&str] = &["", ".js", ".cjs", ".mjs", ".ts", ".cts", ".mts"];
116
117#[must_use]
151pub fn detect_from_config(project_root: &Path) -> Option<StackLabel> {
152 detect_for_ecosystem(project_root, None)
153}
154
155pub fn detect_for_ecosystem(project_root: &Path, scope: Option<E>) -> Option<StackLabel> {
166 let files = ProjectFiles::read(project_root)?;
167 let in_scope = |ecosystem: E| scope.is_none_or(|scope| scope.accepts_config(ecosystem));
168 let python_process = scope == Some(E::Python);
169
170 detect_from_rules(project_root, &files, CONFIG_RULES, in_scope)
171 .or_else(|| {
172 in_scope(E::Python)
173 .then(|| python::detect_python_project(project_root, &files, python_process))
174 .flatten()
175 })
176 .or_else(|| detect_from_rules(project_root, &files, LATE_CONFIG_RULES, in_scope))
177}
178
179fn detect_from_rules(
180 project_root: &Path,
181 files: &ProjectFiles,
182 rules: &[ConfigRule],
183 in_scope: impl Fn(E) -> bool,
184) -> Option<StackLabel> {
185 rules
186 .iter()
187 .filter(|(_, _, ecosystem)| in_scope(*ecosystem))
188 .find(|(matcher, _, _)| rule_matches(project_root, files, *matcher))
189 .map(|(_, label, _)| label.clone())
190}
191
192fn rule_matches(project_root: &Path, files: &ProjectFiles, matcher: ConfigMatch) -> bool {
193 match matcher {
194 Exact(name) => files.contains_exact(name),
195 Prefix(prefix) => files.contains_prefix(prefix),
196 AllOf(paths) => paths
197 .iter()
198 .all(|path| files.contains_path(project_root, path)),
199 Extension(extension) => files.contains_extension(extension),
200 }
201}
202
203pub(super) fn matches_config_name_prefix(name: &str, pattern: &str) -> bool {
204 name.strip_prefix(pattern)
205 .is_some_and(|suffix| COMMON_CONFIG_SUFFIXES.contains(&suffix))
206}