Skip to content

Commit 0d29312

Browse files
committed
Implement package description checking
1 parent 52fdd22 commit 0d29312

File tree

34 files changed

+337
-9
lines changed

34 files changed

+337
-9
lines changed

README.md

+5
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,11 @@ The following checks are performed when calling the binary:
8080
Evaluate Nixpkgs with `system` set to `x86_64-linux` and check that:
8181
- For each package directory, the `pkgs.${name}` attribute must be defined as `callPackage pkgs/by-name/${shard}/${name}/package.nix args` for some `args`.
8282
- For each package directory, `pkgs.lib.isDerivation pkgs.${name}` must be `true`.
83+
- For each top-level attribute, `meta.description` must:
84+
- Start with a capital letter
85+
- Not start with an article (a/an/the)
86+
- Not start with the package name
87+
- Not end with punctuation
8388

8489
### Ratchet checks
8590

src/eval.nix

+12-1
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,18 @@ let
5959
else
6060
{
6161
AttributeSet = {
62-
is_derivation = pkgs.lib.isDerivation value;
62+
derivation =
63+
if pkgs.lib.isDerivation value then
64+
{
65+
Derivation = {
66+
pname = pkgs.lib.getName value;
67+
description = value.meta.description or null;
68+
};
69+
}
70+
else
71+
{
72+
NonDerivation = null;
73+
};
6374
definition_variant =
6475
if !value ? _callPackageVariant then
6576
{ ManualDefinition.is_semantic_call_package = false; }

src/eval.rs

+117-7
Original file line numberDiff line numberDiff line change
@@ -79,12 +79,22 @@ pub enum AttributeVariant {
7979
NonAttributeSet,
8080
AttributeSet {
8181
/// Whether the attribute is a derivation (`lib.isDerivation`)
82-
is_derivation: bool,
82+
derivation: Derivation,
8383
/// The type of `callPackage` used.
8484
definition_variant: DefinitionVariant,
8585
},
8686
}
8787

88+
/// Info about a derivation
89+
#[derive(Deserialize)]
90+
pub enum Derivation {
91+
NonDerivation,
92+
Derivation {
93+
pname: String,
94+
description: Option<String>,
95+
},
96+
}
97+
8898
#[derive(Deserialize)]
8999
pub enum DefinitionVariant {
90100
/// An automatic definition by the `pkgs/by-name` overlay, though it's detected using the
@@ -261,6 +271,61 @@ pub fn check_values(
261271
}))
262272
}
263273

274+
/// Check the validity of a package description
275+
fn check_description(pname: &str, description: &Option<String>) -> ratchet::PackageDescription {
276+
if let Some(text) = description {
277+
let lowercased = text.to_lowercase();
278+
ratchet::PackageDescription {
279+
not_capitalised: {
280+
if let Some(first) = text.chars().next() {
281+
if first.is_lowercase() {
282+
Loose(text.to_owned())
283+
} else {
284+
Tight
285+
}
286+
} else {
287+
Tight
288+
}
289+
},
290+
starts_with_article: {
291+
if lowercased.starts_with("a ")
292+
|| lowercased.starts_with("an ")
293+
|| lowercased.starts_with("the ")
294+
{
295+
Loose(text.to_owned())
296+
} else {
297+
Tight
298+
}
299+
},
300+
starts_with_package_name: {
301+
if lowercased.starts_with(&pname.to_lowercase()) {
302+
Loose(text.to_owned())
303+
} else {
304+
Tight
305+
}
306+
},
307+
ends_with_punctuation: {
308+
if let Some(last) = text.chars().last() {
309+
if last.is_ascii_punctuation() {
310+
Loose(text.to_owned())
311+
} else {
312+
Tight
313+
}
314+
} else {
315+
Tight
316+
}
317+
},
318+
}
319+
} else {
320+
ratchet::PackageDescription {
321+
not_capitalised: Tight,
322+
starts_with_article: Tight,
323+
starts_with_package_name: Tight,
324+
ends_with_punctuation: Tight,
325+
}
326+
}
327+
}
328+
264329
/// Handle the evaluation result for an attribute in `pkgs/by-name`, making it a validation result.
265330
fn by_name(
266331
nix_file_store: &mut NixFileStore,
@@ -279,6 +344,27 @@ fn by_name(
279344
.into()
280345
};
281346

347+
let description_ratchet = match by_name_attribute {
348+
Existing(AttributeInfo {
349+
attribute_variant:
350+
AttributeVariant::AttributeSet {
351+
derivation:
352+
Derivation::Derivation {
353+
pname: ref name,
354+
ref description,
355+
},
356+
..
357+
},
358+
..
359+
}) => check_description(name, description),
360+
_ => ratchet::PackageDescription {
361+
not_capitalised: NonApplicable,
362+
starts_with_article: NonApplicable,
363+
starts_with_package_name: NonApplicable,
364+
ends_with_punctuation: NonApplicable,
365+
},
366+
};
367+
282368
// At this point we know that `pkgs/by-name/fo/foo/package.nix` has to exists. This match
283369
// decides whether the attribute `foo` is defined accordingly and whether a legacy manual
284370
// definition could be removed.
@@ -308,16 +394,17 @@ fn by_name(
308394
// And it's an attribute set, which allows us to get more information about it
309395
attribute_variant:
310396
AttributeVariant::AttributeSet {
311-
is_derivation,
397+
derivation,
312398
definition_variant,
313399
},
314400
location,
315401
}) => {
316402
// Only derivations are allowed in `pkgs/by-name`.
317-
let is_derivation_result = if is_derivation {
318-
Success(())
319-
} else {
320-
to_validation(ByNameErrorKind::NonDerivation).map(|_| ())
403+
let is_derivation_result = match derivation {
404+
Derivation::Derivation { .. } => Success(()),
405+
Derivation::NonDerivation => {
406+
to_validation(ByNameErrorKind::NonDerivation).map(|_| ())
407+
}
321408
};
322409

323410
// If the definition looks correct
@@ -399,6 +486,7 @@ fn by_name(
399486
// once at the end with a map.
400487
manual_definition_result.map(|manual_definition| ratchet::Package {
401488
manual_definition,
489+
description: description_ratchet,
402490
uses_by_name: Tight,
403491
}),
404492
)
@@ -477,6 +565,27 @@ fn handle_non_by_name_attribute(
477565
use ratchet::RatchetState::*;
478566
use NonByNameAttribute::*;
479567

568+
let description_ratchet = match non_by_name_attribute {
569+
EvalSuccess(AttributeInfo {
570+
attribute_variant:
571+
AttributeVariant::AttributeSet {
572+
derivation:
573+
Derivation::Derivation {
574+
pname: ref name,
575+
ref description,
576+
},
577+
..
578+
},
579+
..
580+
}) => check_description(name, description),
581+
_ => ratchet::PackageDescription {
582+
not_capitalised: NonApplicable,
583+
starts_with_article: NonApplicable,
584+
starts_with_package_name: NonApplicable,
585+
ends_with_punctuation: NonApplicable,
586+
},
587+
};
588+
480589
// The ratchet state whether this attribute uses `pkgs/by-name`.
481590
//
482591
// This is never `Tight`, because we only either:
@@ -504,7 +613,7 @@ fn handle_non_by_name_attribute(
504613
// are. Anything else can't be in `pkgs/by-name`.
505614
attribute_variant: AttributeVariant::AttributeSet {
506615
// As of today, non-derivation attribute sets can't be in `pkgs/by-name`.
507-
is_derivation: true,
616+
derivation: Derivation::Derivation { .. },
508617
// Of the two definition variants, really only the manual one makes sense here.
509618
//
510619
// Special cases are:
@@ -601,5 +710,6 @@ fn handle_non_by_name_attribute(
601710
// ourselves all the time to define `manual_definition`, just set it once at the end here.
602711
manual_definition: Tight,
603712
uses_by_name,
713+
description: description_ratchet,
604714
}))
605715
}

src/nixpkgs_problem.rs

+27
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ pub enum NixpkgsProblem {
1919
NixFile(NixFileError),
2020
TopLevelPackage(TopLevelPackageError),
2121
NixEval(NixEvalError),
22+
Description(DescriptionError),
2223
}
2324

2425
/// A file structure error involving a shard (e.g. `fo` is the shard in the path `pkgs/by-name/fo/foo/package.nix`)
@@ -146,6 +147,23 @@ pub struct NixEvalError {
146147
pub stderr: String,
147148
}
148149

150+
/// An error relating to the description of a package
151+
#[derive(Clone)]
152+
pub struct DescriptionError {
153+
pub package_name: String,
154+
pub description: String,
155+
pub kind: DescriptionErrorKind,
156+
}
157+
158+
/// The kind of description problem
159+
#[derive(Clone)]
160+
pub enum DescriptionErrorKind {
161+
NotCapitalised,
162+
StartsWithArticle,
163+
StartsWithPackageName,
164+
EndsWithPunctuation,
165+
}
166+
149167
impl fmt::Display for NixpkgsProblem {
150168
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
151169
match self {
@@ -440,6 +458,15 @@ impl fmt::Display for NixpkgsProblem {
440458
f.write_str(stderr)?;
441459
write!(f, "- Nix evaluation failed for some package in `pkgs/by-name`, see error above")
442460
},
461+
NixpkgsProblem::Description(DescriptionError { package_name, description, kind }) => {
462+
let text = match kind {
463+
DescriptionErrorKind::NotCapitalised => "is not capitalised",
464+
DescriptionErrorKind::StartsWithArticle => "starts with an article (the/a/an)",
465+
DescriptionErrorKind::StartsWithPackageName => "starts with the package name",
466+
DescriptionErrorKind::EndsWithPunctuation => "ends with punctuation",
467+
};
468+
write!(f, "- Description for package {package_name} (\"{description}\") {text}")
469+
},
443470
}
444471
}
445472
}

src/ratchet.rs

+99-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33
//! Each type has a `compare` method that validates the ratchet checks for that item.
44
55
use crate::nix_file::CallPackageArgumentInfo;
6-
use crate::nixpkgs_problem::{NixpkgsProblem, TopLevelPackageError};
6+
use crate::nixpkgs_problem::DescriptionErrorKind;
7+
use crate::nixpkgs_problem::{DescriptionError, NixpkgsProblem, TopLevelPackageError};
78
use crate::validation::{self, Validation, Validation::Success};
89
use relative_path::RelativePathBuf;
910
use std::collections::HashMap;
@@ -37,6 +38,9 @@ pub struct Package {
3738

3839
/// The ratchet value for the check for new packages using pkgs/by-name
3940
pub uses_by_name: RatchetState<UsesByName>,
41+
42+
/// The package description is valid
43+
pub description: PackageDescription,
4044
}
4145

4246
impl Package {
@@ -53,6 +57,11 @@ impl Package {
5357
optional_from.map(|x| &x.uses_by_name),
5458
&to.uses_by_name,
5559
),
60+
PackageDescription::compare(
61+
name,
62+
optional_from.map(|x| &x.description),
63+
&to.description,
64+
),
5665
])
5766
}
5867
}
@@ -166,3 +175,92 @@ impl ToNixpkgsProblem for UsesByName {
166175
})
167176
}
168177
}
178+
179+
/// The ratchet value of an attribute for the check that the description is valid for new packages.
180+
///
181+
/// This checks that new packages don't use an indefinite article in `meta.description`.
182+
pub struct PackageDescription {
183+
pub not_capitalised: RatchetState<DescriptionNotCapitalised>,
184+
pub starts_with_article: RatchetState<DescriptionStartsWithArticle>,
185+
pub starts_with_package_name: RatchetState<DescriptionStartsWithPackageName>,
186+
pub ends_with_punctuation: RatchetState<DescriptionEndsWithPunctuation>,
187+
}
188+
189+
impl PackageDescription {
190+
pub fn compare(name: &str, optional_from: Option<&Self>, to: &Self) -> Validation<()> {
191+
validation::sequence_([
192+
RatchetState::<DescriptionNotCapitalised>::compare(
193+
name,
194+
optional_from.map(|x| &x.not_capitalised),
195+
&to.not_capitalised,
196+
),
197+
RatchetState::<DescriptionStartsWithArticle>::compare(
198+
name,
199+
optional_from.map(|x| &x.starts_with_article),
200+
&to.starts_with_article,
201+
),
202+
RatchetState::<DescriptionStartsWithPackageName>::compare(
203+
name,
204+
optional_from.map(|x| &x.starts_with_package_name),
205+
&to.starts_with_package_name,
206+
),
207+
RatchetState::<DescriptionEndsWithPunctuation>::compare(
208+
name,
209+
optional_from.map(|x| &x.ends_with_punctuation),
210+
&to.ends_with_punctuation,
211+
),
212+
])
213+
}
214+
}
215+
216+
pub enum DescriptionNotCapitalised {}
217+
pub enum DescriptionStartsWithArticle {}
218+
pub enum DescriptionStartsWithPackageName {}
219+
pub enum DescriptionEndsWithPunctuation {}
220+
221+
impl ToDescriptionErrorKind for DescriptionNotCapitalised {
222+
fn to_description_error_kind() -> DescriptionErrorKind {
223+
DescriptionErrorKind::NotCapitalised
224+
}
225+
}
226+
227+
impl ToDescriptionErrorKind for DescriptionStartsWithArticle {
228+
fn to_description_error_kind() -> DescriptionErrorKind {
229+
DescriptionErrorKind::StartsWithArticle
230+
}
231+
}
232+
233+
impl ToDescriptionErrorKind for DescriptionStartsWithPackageName {
234+
fn to_description_error_kind() -> DescriptionErrorKind {
235+
DescriptionErrorKind::StartsWithPackageName
236+
}
237+
}
238+
239+
impl ToDescriptionErrorKind for DescriptionEndsWithPunctuation {
240+
fn to_description_error_kind() -> DescriptionErrorKind {
241+
DescriptionErrorKind::EndsWithPunctuation
242+
}
243+
}
244+
245+
pub trait ToDescriptionErrorKind {
246+
fn to_description_error_kind() -> DescriptionErrorKind;
247+
}
248+
249+
impl<T> ToNixpkgsProblem for T
250+
where
251+
T: ToDescriptionErrorKind,
252+
{
253+
type ToContext = String;
254+
255+
fn to_nixpkgs_problem(
256+
name: &str,
257+
_optional_from: Option<()>,
258+
description: &Self::ToContext,
259+
) -> NixpkgsProblem {
260+
NixpkgsProblem::Description(DescriptionError {
261+
package_name: name.to_owned(),
262+
description: description.to_owned(),
263+
kind: T::to_description_error_kind(),
264+
})
265+
}
266+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
import <test-nixpkgs> { root = ./.; }
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Validated successfully

0 commit comments

Comments
 (0)