1use std::{
4 collections::HashSet,
5 fmt::Display,
6 fs::{metadata, read_dir},
7 path::Path,
8};
9
10use alpm_types::{MetadataFileName, PKGBUILD_FILE_NAME, SRCINFO_FILE_NAME};
11use clap::ValueEnum;
12use serde::{Deserialize, Serialize};
13use strum::{Display as StrumDisplay, VariantArray};
14
15use crate::Error;
16
17#[derive(Clone, Debug, PartialEq)]
31pub struct ScopedName {
32 scope: LintScope,
33 name: &'static str,
34}
35
36impl ScopedName {
37 pub fn new(scope: LintScope, name: &'static str) -> Self {
39 Self { scope, name }
40 }
41}
42
43impl Display for ScopedName {
44 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
45 write!(f, "{}::{}", self.scope, self.name)
46 }
47}
48
49#[derive(
56 Clone, Copy, Debug, Deserialize, PartialEq, Serialize, StrumDisplay, ValueEnum, VariantArray,
57)]
58#[strum(serialize_all = "snake_case")]
59pub enum LintScope {
60 SourceRepository,
71 Package,
82 BuildInfo,
86 PackageBuild,
90 PackageInfo,
94 SourceInfo,
98}
99
100impl LintScope {
101 pub fn contains(&self, other: &LintScope) -> bool {
119 match self {
120 LintScope::SourceRepository => match other {
122 LintScope::SourceRepository | LintScope::SourceInfo | LintScope::PackageBuild => {
123 true
124 }
125 LintScope::BuildInfo | LintScope::PackageInfo | LintScope::Package => false,
126 },
127 LintScope::Package => match other {
129 LintScope::Package | LintScope::PackageBuild | LintScope::PackageInfo => true,
130 LintScope::BuildInfo | LintScope::SourceRepository | LintScope::SourceInfo => false,
131 },
132 LintScope::BuildInfo
134 | LintScope::PackageBuild
135 | LintScope::PackageInfo
136 | LintScope::SourceInfo => self == other,
137 }
138 }
139
140 pub fn detect(path: &Path) -> Result<LintScope, Error> {
153 let metadata = metadata(path).map_err(|source| Error::IoPath {
155 path: path.to_owned(),
156 context: "getting metadata of path",
157 source,
158 })?;
159
160 if metadata.is_file() {
162 let filename = path.file_name().ok_or(Error::NoLintScope {
163 path: path.to_owned(),
164 })?;
165
166 if filename == alpm_types::PKGBUILD_FILE_NAME {
168 return Ok(LintScope::PackageBuild);
169 } else if filename == alpm_types::SRCINFO_FILE_NAME {
170 return Ok(LintScope::SourceInfo);
171 } else if filename == Into::<&'static str>::into(MetadataFileName::BuildInfo) {
173 return Ok(LintScope::BuildInfo);
174 } else if filename == Into::<&'static str>::into(MetadataFileName::PackageInfo) {
175 return Ok(LintScope::PackageInfo);
176 } else {
177 return Err(Error::NoLintScope {
178 path: path.to_path_buf(),
179 });
180 }
181 }
182
183 let entries = read_dir(path).map_err(|source| Error::IoPath {
187 path: path.to_owned(),
188 context: "read directory entries",
189 source,
190 })?;
191
192 let mut filenames = HashSet::new();
193
194 for entry in entries {
197 let entry = entry.map_err(|source| Error::IoPath {
198 path: path.to_owned(),
199 context: "read a specific directory entries",
200 source,
201 })?;
202 let entry_path = entry.path();
203 let metadata = entry.metadata().map_err(|source| Error::IoPath {
204 path: entry_path.to_owned(),
205 context: "getting metadata of file",
206 source,
207 })?;
208
209 if !metadata.is_file() {
211 continue;
212 }
213
214 let Some(filename) = entry_path.file_name() else {
215 continue;
216 };
217 filenames.insert(filename.to_string_lossy().to_string());
218 }
219
220 if filenames.contains(PKGBUILD_FILE_NAME) && filenames.contains(SRCINFO_FILE_NAME) {
221 Ok(LintScope::SourceRepository)
222 } else if filenames.contains(MetadataFileName::BuildInfo.into())
223 && filenames.contains(MetadataFileName::PackageInfo.into())
224 {
225 Ok(LintScope::Package)
226 } else if filenames.contains(PKGBUILD_FILE_NAME) {
227 Ok(LintScope::PackageBuild)
228 } else if filenames.contains(SRCINFO_FILE_NAME) {
229 Ok(LintScope::SourceInfo)
230 } else if filenames.contains(MetadataFileName::BuildInfo.into()) {
231 Ok(LintScope::BuildInfo)
232 } else if filenames.contains(MetadataFileName::PackageInfo.into()) {
233 Ok(LintScope::PackageInfo)
234 } else {
235 Err(Error::NoLintScope {
236 path: path.to_path_buf(),
237 })
238 }
239 }
240
241 pub fn is_single_file(&self) -> bool {
243 match self {
244 LintScope::SourceRepository | LintScope::Package => false,
245 LintScope::BuildInfo
246 | LintScope::PackageBuild
247 | LintScope::PackageInfo
248 | LintScope::SourceInfo => true,
249 }
250 }
251}
252
253#[cfg(test)]
254mod tests {
255 use std::fs::File;
256
257 use rstest::rstest;
258 use testresult::{TestError, TestResult};
259
260 use super::*;
261
262 #[rstest]
264 #[case::package(vec!["PKGBUILD", ".SRCINFO"], LintScope::SourceRepository)]
265 #[case::package_with_other_files(vec!["test_file", "PKGBUILD", ".SRCINFO", ".BUILDINFO", ".PKGINFO"], LintScope::SourceRepository)]
266 #[case::package_build(vec!["PKGBUILD"], LintScope::PackageBuild)]
267 #[case::source_info(vec![".SRCINFO"], LintScope::SourceInfo)]
268 #[case::source_repo(vec![".BUILDINFO", ".PKGINFO"], LintScope::Package)]
269 #[case::source_repo_with_other_files(vec!["test_file", "PKGBUILD", ".BUILDINFO", ".PKGINFO"], LintScope::Package)]
270 #[case::build_info(vec![".BUILDINFO"], LintScope::BuildInfo)]
271 #[case::package_info(vec![".PKGINFO"], LintScope::PackageInfo)]
272 fn detect_scope_in_directory(
273 #[case] files: Vec<&'static str>,
274 #[case] expected: LintScope,
275 ) -> TestResult<()> {
276 let tmp_dir = tempfile::tempdir()?;
278
279 for name in &files {
281 let path = tmp_dir.path().join(name);
282 File::create(&path)?;
283 }
284
285 let scope = LintScope::detect(tmp_dir.path())?;
286
287 assert_eq!(
288 scope, expected,
289 "Expected '{expected}' scope for file set {files:?}"
290 );
291
292 Ok(())
293 }
294
295 #[rstest]
297 #[case::unknown_files(vec!["test_file", "test_file2"])]
298 #[case::no_files(vec![])]
299 fn fail_to_detect_scope_in_directory(#[case] files: Vec<&'static str>) -> TestResult<()> {
300 let tmp_dir = tempfile::tempdir()?;
302
303 for name in &files {
305 let path = tmp_dir.path().join(name);
306 File::create(&path)?;
307 }
308
309 let error = match LintScope::detect(tmp_dir.path()) {
310 Ok(scope) => {
311 return Err(TestError::from(format!(
312 "Expected an error for scope detection for file set {files:?}, got {scope}"
313 )));
314 }
315 Err(err) => err,
316 };
317
318 assert!(
319 matches!(error, Error::NoLintScope { .. }),
320 "Expected 'NoLintScope' error for file set {files:?}"
321 );
322
323 Ok(())
324 }
325
326 #[rstest]
328 #[case::package_build("PKGBUILD", LintScope::PackageBuild)]
329 #[case::source_info(".SRCINFO", LintScope::SourceInfo)]
330 #[case::build_info(".BUILDINFO", LintScope::BuildInfo)]
331 #[case::package_info(".PKGINFO", LintScope::PackageInfo)]
332 fn detect_scope_of_file(
333 #[case] file: &'static str,
334 #[case] expected: LintScope,
335 ) -> TestResult<()> {
336 let tmp_dir = tempfile::tempdir()?;
338
339 let path = tmp_dir.path().join(file);
341 File::create(&path)?;
342
343 let scope = LintScope::detect(&path)?;
344
345 assert_eq!(
346 scope, expected,
347 "Expected '{expected}' scope for file {file:?}"
348 );
349 assert!(scope.is_single_file());
350
351 Ok(())
352 }
353}