Skip to content

Commit dd11693

Browse files
author
Nichol Yip
committed
Implemented linting for build options
Signed-off-by: Nichol Yip <[email protected]>
1 parent 07c0bf5 commit dd11693

File tree

3 files changed

+166
-128
lines changed

3 files changed

+166
-128
lines changed

crates/spk-schema/src/build_spec.rs

+4-3
Original file line numberDiff line numberDiff line change
@@ -485,16 +485,17 @@ impl<'de> serde::de::Visitor<'de> for BuildSpecVisitor {
485485
match key.as_str() {
486486
"script" => unchecked.script = map.next_value::<Script>()?,
487487
"options" => {
488-
unchecked.options = map.next_value::<Vec<Opt>>()?;
488+
let linted_opts = map.next_value::<Vec<LintedItem<Opt>>>()?;
489489
let mut unique_options = HashSet::new();
490-
for opt in unchecked.options.iter() {
491-
let full_name = opt.full_name();
490+
for opt in linted_opts.iter() {
491+
let full_name = opt.item.full_name();
492492
if unique_options.contains(full_name) {
493493
return Err(serde::de::Error::custom(format!(
494494
"build option was specified more than once: {full_name}",
495495
)));
496496
}
497497
unique_options.insert(full_name);
498+
self.lints.extend(opt.lints.clone());
498499
}
499500
}
500501
"variants" => {

crates/spk-schema/src/lints.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
// Copyright (c) Sony Pictures Imageworks, et al.
1+
// Copyright (c) Contributors to the SPK project.
22
// SPDX-License-Identifier: Apache-2.0
3-
// https://github.com/imageworks/spk
3+
// https://github.com/spkenv/spk
44

55
use std::sync::Arc;
66

crates/spk-schema/src/option.rs

+160-123
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ use spk_schema_foundation::option_map::Stringified;
1212
use spk_schema_foundation::version::{Compat, IncompatibleReason, VarOptionProblem};
1313
use spk_schema_foundation::IsDefault;
1414
use spk_schema_ident::{NameAndValue, PinnableValue, RangeIdent};
15+
use struct_field_names_as_array::FieldNamesAsArray;
1516

1617
use crate::foundation::name::{OptName, OptNameBuf, PkgName, PkgNameBuf};
1718
use crate::foundation::version::{CompatRule, Compatibility};
@@ -25,7 +26,7 @@ use crate::ident::{
2526
RequestedBy,
2627
VarRequest,
2728
};
28-
use crate::{Error, Result};
29+
use crate::{Error, Lint, LintedItem, Lints, Result, UnknownKey};
2930

3031
#[cfg(test)]
3132
#[path = "./option_test.rs"]
@@ -190,142 +191,178 @@ impl TryFrom<Request> for Opt {
190191
}
191192
}
192193

194+
/// This visitor captures all fields that could be valid
195+
/// for any option, before deciding at the end which variant
196+
/// to actually build. We ignore any unrecognized field anyway,
197+
/// but additionally any field that's recognized must be valid
198+
/// even if it's not going to be used.
199+
///
200+
/// The purpose of this setup is to enable more meaningful errors
201+
/// for invalid values that contain original source positions. In
202+
/// order to achieve this we must parse and validate each field with
203+
/// the appropriate type as they are visited - which disqualifies the
204+
/// existing approach to untagged enums which read all fields first
205+
/// and then goes back and checks them once the variant is determined
206+
#[derive(Default, FieldNamesAsArray)]
207+
struct UncheckedOptVisitor {
208+
// PkgOpt
209+
pkg: Option<PkgNameWithComponents>,
210+
prerelease_policy: Option<PreReleasePolicy>,
211+
212+
// VarOpt
213+
var: Option<OptNameBuf>,
214+
choices: Option<IndexSet<String>>,
215+
inheritance: Option<Inheritance>,
216+
compat: Option<Compat>,
217+
218+
// Both
219+
default: Option<String>,
220+
value: Option<String>,
221+
description: Option<String>,
222+
223+
// Lints
224+
#[field_names_as_array(skip)]
225+
lints: Vec<Lint>,
226+
}
227+
228+
struct CheckedOptVisitor {
229+
opt: Opt,
230+
lints: Vec<Lint>,
231+
}
232+
233+
impl Lints for CheckedOptVisitor {
234+
fn lints(&mut self) -> Vec<Lint> {
235+
std::mem::take(&mut self.lints)
236+
}
237+
}
238+
239+
impl From<CheckedOptVisitor> for Opt {
240+
fn from(value: CheckedOptVisitor) -> Self {
241+
value.opt
242+
}
243+
}
244+
193245
impl<'de> Deserialize<'de> for Opt {
194246
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
195247
where
196-
D: serde::Deserializer<'de>,
248+
D: serde::de::Deserializer<'de>,
197249
{
198-
/// This visitor captures all fields that could be valid
199-
/// for any option, before deciding at the end which variant
200-
/// to actually build. We ignore any unrecognized field anyway,
201-
/// but additionally any field that's recognized must be valid
202-
/// even if it's not going to be used.
203-
///
204-
/// The purpose of this setup is to enable more meaningful errors
205-
/// for invalid values that contain original source positions. In
206-
/// order to achieve this we must parse and validate each field with
207-
/// the appropriate type as they are visited - which disqualifies the
208-
/// existing approach to untagged enums which read all fields first
209-
/// and then goes back and checks them once the variant is determined
210-
#[derive(Default)]
211-
struct OptVisitor {
212-
// PkgOpt
213-
pkg: Option<PkgNameWithComponents>,
214-
prerelease_policy: Option<PreReleasePolicy>,
215-
216-
// VarOpt
217-
var: Option<OptNameBuf>,
218-
choices: Option<IndexSet<String>>,
219-
inheritance: Option<Inheritance>,
220-
compat: Option<Compat>,
221-
222-
// Both
223-
default: Option<String>,
224-
value: Option<String>,
225-
description: Option<String>,
226-
}
250+
Ok(deserializer
251+
.deserialize_map(UncheckedOptVisitor::default())?
252+
.into())
253+
}
254+
}
227255

228-
impl<'de> serde::de::Visitor<'de> for OptVisitor {
229-
type Value = Opt;
256+
impl<'de> Deserialize<'de> for LintedItem<Opt> {
257+
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
258+
where
259+
D: serde::de::Deserializer<'de>,
260+
{
261+
Ok(deserializer
262+
.deserialize_map(UncheckedOptVisitor::default())?
263+
.into())
264+
}
265+
}
230266

231-
fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
232-
f.write_str("a pkg or var option")
233-
}
267+
impl<'de> serde::de::Visitor<'de> for UncheckedOptVisitor {
268+
type Value = CheckedOptVisitor;
234269

235-
fn visit_map<A>(mut self, mut map: A) -> std::result::Result<Self::Value, A::Error>
236-
where
237-
A: serde::de::MapAccess<'de>,
238-
{
239-
let check_existing_default = |v: &OptVisitor| -> std::result::Result<(), A::Error> {
240-
if v.default.is_some() {
241-
Err(serde::de::Error::custom("option cannot specify both the 'default' field and a default value in the form <name>/<default>"))
242-
} else {
243-
Ok(())
270+
fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
271+
f.write_str("a pkg or var option")
272+
}
273+
274+
fn visit_map<A>(mut self, mut map: A) -> std::result::Result<Self::Value, A::Error>
275+
where
276+
A: serde::de::MapAccess<'de>,
277+
{
278+
let check_existing_default =
279+
|v: &UncheckedOptVisitor| -> std::result::Result<(), A::Error> {
280+
if v.default.is_some() {
281+
Err(serde::de::Error::custom("option cannot specify both the 'default' field and a default value in the form <name>/<default>"))
282+
} else {
283+
Ok(())
284+
}
285+
};
286+
287+
while let Some(mut key) = map.next_key::<Stringified>()? {
288+
key.make_ascii_lowercase();
289+
match key.as_str() {
290+
"pkg" => {
291+
let pkg_name_with_components: PkgNameWithComponents = map.next_value()?;
292+
if pkg_name_with_components.default.is_some() {
293+
check_existing_default(&self)?;
294+
self.default.clone_from(&pkg_name_with_components.default);
244295
}
245-
};
246-
247-
while let Some(mut key) = map.next_key::<Stringified>()? {
248-
key.make_ascii_lowercase();
249-
match key.as_str() {
250-
"pkg" => {
251-
let pkg_name_with_components: PkgNameWithComponents =
252-
map.next_value()?;
253-
if pkg_name_with_components.default.is_some() {
254-
check_existing_default(&self)?;
255-
self.default.clone_from(&pkg_name_with_components.default);
256-
}
257-
self.pkg = Some(pkg_name_with_components);
258-
}
259-
"prereleasepolicy" => {
260-
self.prerelease_policy = Some(map.next_value::<PreReleasePolicy>()?)
261-
}
262-
"var" => {
263-
let NameAndValue(name, value) = map.next_value()?;
264-
self.var = Some(name);
265-
if value.is_some() {
266-
check_existing_default(&self)?;
267-
}
268-
self.default = value;
269-
}
270-
"choices" => {
271-
self.choices = Some(
272-
map.next_value::<Vec<Stringified>>()?
273-
.into_iter()
274-
.map(|s| s.0)
275-
.collect(),
276-
)
277-
}
278-
"inheritance" => self.inheritance = Some(map.next_value::<Inheritance>()?),
279-
"default" => {
280-
check_existing_default(&self)?;
281-
self.default = Some(map.next_value::<Stringified>()?.0);
282-
}
283-
"static" => self.value = Some(map.next_value::<Stringified>()?.0),
284-
"description" => {
285-
self.description = Some(map.next_value::<Stringified>()?.0);
286-
}
287-
"compat" => {
288-
self.compat = Some(map.next_value::<Compat>()?);
289-
}
290-
_ => {
291-
// unrecognized fields are explicitly ignored in case
292-
// they were added in a newer version of spk. We assume
293-
// that if the api has not been versioned then the desire
294-
// is to continue working in this older version
295-
map.next_value::<serde::de::IgnoredAny>()?;
296-
}
296+
self.pkg = Some(pkg_name_with_components);
297+
}
298+
"prereleasepolicy" => {
299+
self.prerelease_policy = Some(map.next_value::<PreReleasePolicy>()?)
300+
}
301+
"var" => {
302+
let NameAndValue(name, value) = map.next_value()?;
303+
self.var = Some(name);
304+
if value.is_some() {
305+
check_existing_default(&self)?;
297306
}
307+
self.default = value;
298308
}
299-
300-
match (self.pkg, self.var) {
301-
(Some(pkg), None) => Ok(Opt::Pkg(PkgOpt {
302-
pkg: pkg.name,
303-
components: pkg.components,
304-
prerelease_policy: self.prerelease_policy,
305-
required_compat: Default::default(),
306-
default: self.default.unwrap_or_default(),
307-
value: self.value,
308-
})),
309-
(None, Some(var)) =>Ok(Opt::Var(VarOpt {
310-
var,
311-
choices: self.choices.unwrap_or_default(),
312-
inheritance: self.inheritance.unwrap_or_default(),
313-
default: self.default.unwrap_or_default(),
314-
description: self.description,
315-
compat: self.compat,
316-
value: self.value,
317-
})),
318-
(Some(_), Some(_)) => Err(serde::de::Error::custom(
319-
"could not determine option type, it may only contain one of the `pkg` or `var` fields"
320-
)),
321-
(None, None) => Err(serde::de::Error::custom(
322-
"could not determine option type, it must include either a `pkg` or `var` field"
323-
)),
309+
"choices" => {
310+
self.choices = Some(
311+
map.next_value::<Vec<Stringified>>()?
312+
.into_iter()
313+
.map(|s| s.0)
314+
.collect(),
315+
)
316+
}
317+
"inheritance" => self.inheritance = Some(map.next_value::<Inheritance>()?),
318+
"default" => {
319+
check_existing_default(&self)?;
320+
self.default = Some(map.next_value::<Stringified>()?.0);
321+
}
322+
"static" => self.value = Some(map.next_value::<Stringified>()?.0),
323+
"description" => {
324+
self.description = Some(map.next_value::<Stringified>()?.0);
325+
}
326+
"compat" => {
327+
self.compat = Some(map.next_value::<Compat>()?);
328+
}
329+
unknown_key => {
330+
self.lints.push(Lint::Key(UnknownKey::new(
331+
unknown_key,
332+
UncheckedOptVisitor::FIELD_NAMES_AS_ARRAY.to_vec(),
333+
)));
334+
map.next_value::<serde::de::IgnoredAny>()?;
324335
}
325336
}
326337
}
327338

328-
deserializer.deserialize_map(OptVisitor::default())
339+
match (self.pkg, self.var) {
340+
(Some(pkg), None) => Ok(CheckedOptVisitor {
341+
opt: Opt::Pkg(PkgOpt {
342+
pkg: pkg.name,
343+
components: pkg.components,
344+
prerelease_policy: self.prerelease_policy,
345+
required_compat: Default::default(),
346+
default: self.default.unwrap_or_default(),
347+
value: self.value,
348+
}),
349+
lints: self.lints.clone(),
350+
}),
351+
(None, Some(var)) => Ok(CheckedOptVisitor {
352+
opt: Opt::Var(VarOpt {
353+
var,
354+
choices: self.choices.unwrap_or_default(),
355+
inheritance: self.inheritance.unwrap_or_default(),
356+
default: self.default.unwrap_or_default(),
357+
description: self.description,
358+
compat: self.compat,
359+
value: self.value,
360+
}),
361+
lints: self.lints.clone(),
362+
}),
363+
(Some(_), Some(_)) => Err(serde::de::Error::custom("could not determine option type, it may only contain one of the `pkg` or `var` fields")),
364+
(None, None) => Err(serde::de::Error::custom("could not determine option type, it must include either a `pkg` or `var` field")),
365+
}
329366
}
330367
}
331368

0 commit comments

Comments
 (0)