Skip to content

Commit

Permalink
Fix negative value parsing on schema attributes (#1031)
Browse files Browse the repository at this point in the history
This commit fixes issue where negative numbers caused parsing error by
`rust_analyzer` however the code compiled correctly and created valid
code.

The issue is fixed for validation attributes and `AnyValue` which is
used in various places. The original issue only reported the issue in
validation attributes.

Fixes #994
  • Loading branch information
juhaku authored Sep 6, 2024
1 parent 18d004a commit ed255a1
Show file tree
Hide file tree
Showing 7 changed files with 266 additions and 92 deletions.
43 changes: 8 additions & 35 deletions utoipa-gen/src/component/features.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
use std::{fmt::Display, mem, str::FromStr};
use std::{fmt::Display, mem};

use proc_macro2::{Ident, TokenStream};
use quote::{quote, ToTokens};
use syn::{parse::ParseStream, LitFloat, LitInt};
use syn::parse::ParseStream;

use crate::{
as_tokens_or_diagnostics, parse_utils, schema_type::SchemaType, Diagnostics, OptionExt,
ToTokensDiagnostics,
as_tokens_or_diagnostics, schema_type::SchemaType, Diagnostics, OptionExt, ToTokensDiagnostics,
};

use self::validators::{AboveZeroF64, AboveZeroUsize, IsNumber, IsString, IsVec, ValidatorChain};
Expand All @@ -17,32 +16,6 @@ pub mod attributes;
pub mod validation;
pub mod validators;

/// Parse `LitInt` from parse stream
fn parse_integer<T: FromStr + Display>(input: ParseStream) -> syn::Result<T>
where
<T as FromStr>::Err: Display,
{
parse_utils::parse_next(input, || input.parse::<LitInt>()?.base10_parse())
}

/// Parse any `number`. Tries to parse `LitInt` or `LitFloat` from parse stream.
fn parse_number<T>(input: ParseStream) -> syn::Result<T>
where
T: FromStr,
<T as FromStr>::Err: Display,
{
parse_utils::parse_next(input, || {
let lookup = input.lookahead1();
if lookup.peek(LitInt) {
input.parse::<LitInt>()?.base10_parse()
} else if lookup.peek(LitFloat) {
input.parse::<LitFloat>()?.base10_parse()
} else {
Err(lookup.error())
}
})
}

pub trait FeatureLike: Parse {
fn get_name() -> std::borrow::Cow<'static, str>
where
Expand Down Expand Up @@ -145,7 +118,7 @@ impl Feature {
pub fn validate(&self, schema_type: &SchemaType, type_tree: &TypeTree) -> Option<Diagnostics> {
match self {
Feature::MultipleOf(multiple_of) => multiple_of.validate(
ValidatorChain::new(&IsNumber(schema_type)).next(&AboveZeroF64(multiple_of.0)),
ValidatorChain::new(&IsNumber(schema_type)).next(&AboveZeroF64(&multiple_of.0)),
),
Feature::Maximum(maximum) => maximum.validate(IsNumber(schema_type)),
Feature::Minimum(minimum) => minimum.validate(IsNumber(schema_type)),
Expand All @@ -156,17 +129,17 @@ impl Feature {
exclusive_minimum.validate(IsNumber(schema_type))
}
Feature::MaxLength(max_length) => max_length.validate(
ValidatorChain::new(&IsString(schema_type)).next(&AboveZeroUsize(max_length.0)),
ValidatorChain::new(&IsString(schema_type)).next(&AboveZeroUsize(&max_length.0)),
),
Feature::MinLength(min_length) => min_length.validate(
ValidatorChain::new(&IsString(schema_type)).next(&AboveZeroUsize(min_length.0)),
ValidatorChain::new(&IsString(schema_type)).next(&AboveZeroUsize(&min_length.0)),
),
Feature::Pattern(pattern) => pattern.validate(IsString(schema_type)),
Feature::MaxItems(max_items) => max_items.validate(
ValidatorChain::new(&AboveZeroUsize(max_items.0)).next(&IsVec(type_tree)),
ValidatorChain::new(&AboveZeroUsize(&max_items.0)).next(&IsVec(type_tree)),
),
Feature::MinItems(min_items) => min_items.validate(
ValidatorChain::new(&AboveZeroUsize(min_items.0)).next(&IsVec(type_tree)),
ValidatorChain::new(&AboveZeroUsize(&min_items.0)).next(&IsVec(type_tree)),
),
unsupported => {
const SUPPORTED_VARIANTS: [&str; 10] = [
Expand Down
132 changes: 106 additions & 26 deletions utoipa-gen/src/component/features/validation.rs
Original file line number Diff line number Diff line change
@@ -1,17 +1,91 @@
use proc_macro2::{Ident, Span, TokenStream};
use quote::ToTokens;
use std::str::FromStr;

use proc_macro2::{Ident, Literal, Span, TokenStream, TokenTree};
use quote::{quote, ToTokens};
use syn::parse::ParseStream;
use syn::LitStr;

use crate::{parse_utils, Diagnostics};

use super::validators::Validator;
use super::{impl_feature, parse_integer, parse_number, Feature, Parse, Validate};
use super::{impl_feature, Feature, Parse, Validate};

#[inline]
fn from_str<T: FromStr>(number: &str, span: Span) -> syn::Result<T>
where
<T as std::str::FromStr>::Err: std::fmt::Display,
{
T::from_str(number).map_err(|error| syn::Error::new(span, error))
}

#[derive(Clone)]
#[cfg_attr(feature = "debug", derive(Debug))]
pub struct NumberValue {
minus: bool,
pub lit: Literal,
}

impl NumberValue {
pub fn try_from_str<T>(&self) -> syn::Result<T>
where
T: FromStr,
<T as std::str::FromStr>::Err: std::fmt::Display,
{
let number = if self.minus {
format!("-{}", &self.lit)
} else {
self.lit.to_string()
};

let parsed = from_str::<T>(&number, self.lit.span())?;
Ok(parsed)
}
}

impl syn::parse::Parse for NumberValue {
fn parse(input: ParseStream) -> syn::Result<Self> {
let mut minus = false;
let result = input.step(|cursor| {
let mut rest = *cursor;

while let Some((tt, next)) = rest.token_tree() {
match &tt {
TokenTree::Punct(punct) if punct.as_char() == '-' => {
minus = true;
}
TokenTree::Literal(lit) => return Ok((lit.clone(), next)),
_ => (),
}
rest = next;
}
Err(cursor.error("no `literal` value found after this point"))
})?;

Ok(Self { minus, lit: result })
}
}

impl ToTokens for NumberValue {
fn to_tokens(&self, tokens: &mut TokenStream) {
let punct = if self.minus { Some(quote! {-}) } else { None };
let lit = &self.lit;

tokens.extend(quote! {
#punct #lit
})
}
}

#[inline]
fn parse_next_number_value(input: ParseStream) -> syn::Result<NumberValue> {
use syn::parse::Parse;
parse_utils::parse_next(input, || NumberValue::parse(input))
}

impl_feature! {
#[cfg_attr(feature = "debug", derive(Debug))]
#[derive(Clone)]
pub struct MultipleOf(pub(super) f64, Ident);
pub struct MultipleOf(pub(super) NumberValue, Ident);
}

impl Validate for MultipleOf {
Expand All @@ -26,7 +100,7 @@ impl Validate for MultipleOf {

impl Parse for MultipleOf {
fn parse(input: ParseStream, ident: Ident) -> syn::Result<Self> {
parse_number(input).map(|multiple_of| Self(multiple_of, ident))
parse_next_number_value(input).map(|number| Self(number, ident))
}
}

Expand All @@ -45,7 +119,7 @@ impl From<MultipleOf> for Feature {
impl_feature! {
#[cfg_attr(feature = "debug", derive(Debug))]
#[derive(Clone)]
pub struct Maximum(pub(super) f64, Ident);
pub struct Maximum(pub(super) NumberValue, Ident);
}

impl Validate for Maximum {
Expand All @@ -63,7 +137,7 @@ impl Parse for Maximum {
where
Self: Sized,
{
parse_number(input).map(|maximum| Self(maximum, ident))
parse_next_number_value(input).map(|number| Self(number, ident))
}
}

Expand All @@ -82,12 +156,18 @@ impl From<Maximum> for Feature {
impl_feature! {
#[cfg_attr(feature = "debug", derive(Debug))]
#[derive(Clone)]
pub struct Minimum(f64, Ident);
pub struct Minimum(NumberValue, Ident);
}

impl Minimum {
pub fn new(value: f64, span: Span) -> Self {
Self(value, Ident::new("empty", span))
Self(
NumberValue {
minus: value < 0.0,
lit: Literal::f64_suffixed(value),
},
Ident::new("empty", span),
)
}
}

Expand All @@ -108,7 +188,7 @@ impl Parse for Minimum {
where
Self: Sized,
{
parse_number(input).map(|maximum| Self(maximum, ident))
parse_next_number_value(input).map(|number| Self(number, ident))
}
}

Expand All @@ -127,7 +207,7 @@ impl From<Minimum> for Feature {
impl_feature! {
#[cfg_attr(feature = "debug", derive(Debug))]
#[derive(Clone)]
pub struct ExclusiveMaximum(f64, Ident);
pub struct ExclusiveMaximum(NumberValue, Ident);
}

impl Validate for ExclusiveMaximum {
Expand All @@ -145,7 +225,7 @@ impl Parse for ExclusiveMaximum {
where
Self: Sized,
{
parse_number(input).map(|max| Self(max, ident))
parse_next_number_value(input).map(|number| Self(number, ident))
}
}

Expand All @@ -164,7 +244,7 @@ impl From<ExclusiveMaximum> for Feature {
impl_feature! {
#[cfg_attr(feature = "debug", derive(Debug))]
#[derive(Clone)]
pub struct ExclusiveMinimum(f64, Ident);
pub struct ExclusiveMinimum(NumberValue, Ident);
}

impl Validate for ExclusiveMinimum {
Expand All @@ -182,7 +262,7 @@ impl Parse for ExclusiveMinimum {
where
Self: Sized,
{
parse_number(input).map(|min| Self(min, ident))
parse_next_number_value(input).map(|number| Self(number, ident))
}
}

Expand All @@ -201,7 +281,7 @@ impl From<ExclusiveMinimum> for Feature {
impl_feature! {
#[cfg_attr(feature = "debug", derive(Debug))]
#[derive(Clone)]
pub struct MaxLength(pub(super) usize, Ident);
pub struct MaxLength(pub(super) NumberValue, Ident);
}

impl Validate for MaxLength {
Expand All @@ -219,7 +299,7 @@ impl Parse for MaxLength {
where
Self: Sized,
{
parse_integer(input).map(|max_length| Self(max_length, ident))
parse_next_number_value(input).map(|number| Self(number, ident))
}
}

Expand All @@ -238,7 +318,7 @@ impl From<MaxLength> for Feature {
impl_feature! {
#[cfg_attr(feature = "debug", derive(Debug))]
#[derive(Clone)]
pub struct MinLength(pub(super) usize, Ident);
pub struct MinLength(pub(super) NumberValue, Ident);
}

impl Validate for MinLength {
Expand All @@ -256,7 +336,7 @@ impl Parse for MinLength {
where
Self: Sized,
{
parse_integer(input).map(|max_length| Self(max_length, ident))
parse_next_number_value(input).map(|number| Self(number, ident))
}
}

Expand Down Expand Up @@ -314,7 +394,7 @@ impl From<Pattern> for Feature {
impl_feature! {
#[cfg_attr(feature = "debug", derive(Debug))]
#[derive(Clone)]
pub struct MaxItems(pub(super) usize, Ident);
pub struct MaxItems(pub(super) NumberValue, Ident);
}

impl Validate for MaxItems {
Expand All @@ -332,7 +412,7 @@ impl Parse for MaxItems {
where
Self: Sized,
{
parse_number(input).map(|max_items| Self(max_items, ident))
parse_next_number_value(input).map(|number| Self(number, ident))
}
}

Expand All @@ -351,7 +431,7 @@ impl From<MaxItems> for Feature {
impl_feature! {
#[cfg_attr(feature = "debug", derive(Debug))]
#[derive(Clone)]
pub struct MinItems(pub(super) usize, Ident);
pub struct MinItems(pub(super) NumberValue, Ident);
}

impl Validate for MinItems {
Expand All @@ -369,7 +449,7 @@ impl Parse for MinItems {
where
Self: Sized,
{
parse_number(input).map(|max_items| Self(max_items, ident))
parse_next_number_value(input).map(|number| Self(number, ident))
}
}

Expand All @@ -388,15 +468,15 @@ impl From<MinItems> for Feature {
impl_feature! {
#[cfg_attr(feature = "debug", derive(Debug))]
#[derive(Clone)]
pub struct MaxProperties(usize, ());
pub struct MaxProperties(NumberValue, ());
}

impl Parse for MaxProperties {
fn parse(input: ParseStream, _ident: Ident) -> syn::Result<Self>
where
Self: Sized,
{
parse_integer(input).map(|max_properties| Self(max_properties, ()))
parse_next_number_value(input).map(|number| Self(number, ()))
}
}

Expand All @@ -415,15 +495,15 @@ impl From<MaxProperties> for Feature {
impl_feature! {
#[cfg_attr(feature = "debug", derive(Debug))]
#[derive(Clone)]
pub struct MinProperties(usize, ());
pub struct MinProperties(NumberValue, ());
}

impl Parse for MinProperties {
fn parse(input: ParseStream, _ident: Ident) -> syn::Result<Self>
where
Self: Sized,
{
parse_integer(input).map(|min_properties| Self(min_properties, ()))
parse_next_number_value(input).map(|number| Self(number, ()))
}
}

Expand Down
Loading

0 comments on commit ed255a1

Please sign in to comment.