verty 0.1.1

procedural macro to generate different versions of a type
Documentation
use proc_macro2::Span;
use syn::visit_mut::VisitMut;
use syn::{DeriveInput, GenericParam, Result};

use super::pseudo_macros::ExpandPseudoMacros;
use super::{Args, versioned_ident};
use crate::output::{
	OutputEnum, OutputFields, OutputItemBase, OutputItems, OutputStructOrUnion, OutputVariant,
};
use crate::parse::args::MacroArg;
use crate::parse::helper_attrs::{
	HelperAttrInPlace as _, VersionFilter, VersionedAttr, VersionedWhere,
};
use crate::parse::{
	ParsedEnum, ParsedFields, ParsedGenericParams, ParsedInput, ParsedItemBase,
	ParsedStructOrUnion, StructOrUnion,
};
use crate::util::error_sink::ErrorSink;

pub fn versioned(args: Vec<(Span, MacroArg)>, input: DeriveInput) -> Result<OutputItems> {
	let mut errs = ErrorSink::new();
	let args = Args::from_raw(args, &mut errs);

	let input = ParsedInput::parse_from(input, &args)?;

	match input {
		ParsedInput::StructOrUnion(input) => match input.kind {
			StructOrUnion::Struct => {
				versioned_struct_or_union::<false>(args, input, errs).map(OutputItems::Structs)
			}
			StructOrUnion::Union => {
				versioned_struct_or_union::<true>(args, input, errs).map(OutputItems::Unions)
			}
		},
		ParsedInput::Enum(input) => versioned_enum(args, input, errs).map(OutputItems::Enums),
	}
}

impl ParsedStructOrUnion {
	pub fn highest_mentioned_version(&self) -> Option<usize> {
		self.base
			.highest_mentioned_version
			.max(self.fields.highest_mentioned_version)
	}
}

impl ParsedEnum {
	pub fn highest_mentioned_version(&self) -> Option<usize> {
		std::iter::once(self.base.highest_mentioned_version)
			.chain(
				self.variants
					.iter()
					.map(|var| var.highest_mentioned_version),
			)
			.max()
			.flatten()
	}
}

fn versioned_struct_or_union<const IS_UNION: bool>(
	args: Args,
	input: ParsedStructOrUnion,
	mut errs: ErrorSink,
) -> Result<Vec<OutputStructOrUnion<IS_UNION>>> {
	let mut errs2 = ErrorSink::new();

	let res = versioned_item_base(
		&args,
		input.highest_mentioned_version(),
		input.base,
		&mut errs,
	)
	.map(|(version, base)| {
		let errs = &mut errs2;

		OutputStructOrUnion::<IS_UNION> {
			base,
			fields: versioned_fields(version, &input.fields, &args, errs),
		}
	})
	.collect();

	errs.append(errs2);
	errs.finish_with(|| res)
}

fn versioned_enum(args: Args, input: ParsedEnum, mut errs: ErrorSink) -> Result<Vec<OutputEnum>> {
	let mut errs2 = ErrorSink::new();

	let res = versioned_item_base(
		&args,
		input.highest_mentioned_version(),
		input.base,
		&mut errs,
	)
	.map(|(version, base)| {
		let errs = &mut errs2;

		let variants = input
			.variants
			.iter()
			.filter_map(|variant| {
				let (versions, ()) = &variant.helpers;
				let is_included =
					versions.map_or(true, |VersionFilter(versions)| versions.contains(version));

				is_included.then(|| OutputVariant {
					attrs: VersionedAttr::process_attrs_for_version(
						variant.attrs.clone(),
						version,
						Some(&args),
						errs,
					),
					ident: variant.ident.clone(),
					fields: versioned_fields(version, &variant.fields, &args, errs),
					discriminant: variant.discriminant.as_ref().map(|(_, d)| d.clone()),
				})
			})
			.collect();

		OutputEnum { base, variants }
	})
	.collect();

	errs.append(errs2);
	errs.finish_with(|| res)
}

fn versioned_item_base<'a>(
	args: &'a Args,
	highest_mentioned_version: Option<usize>,
	input: ParsedItemBase,
	errs: &'a mut ErrorSink,
) -> impl Iterator<Item = (usize, OutputItemBase)> + 'a {
	let end = args.end.unwrap_or_else(|| highest_mentioned_version.unwrap_or(args.start));
	(args.start..=end).map(move |version| {
		let mut generic_params = input.generic_params.as_ref().map(
			|ParsedGenericParams {
			     highest_mentioned_version: _,
			     params,
			 }| {
				params
					.iter()
					.filter_map(|(helpers, param)| {
						let (versions, ()) = helpers;
						let is_included = versions
							.map_or(true, |VersionFilter(versions)| versions.contains(version));

						is_included.then(|| {
							let param = param.clone();

							match param {
								GenericParam::Lifetime(mut param) => {
									param.attrs = VersionedAttr::process_attrs_for_version(
										param.attrs,
										version,
										Some(args),
										errs,
									);

									GenericParam::Lifetime(param)
								}
								GenericParam::Type(mut param) => {
									param.attrs = VersionedAttr::process_attrs_for_version(
										param.attrs,
										version,
										Some(args),
										errs,
									);

									GenericParam::Type(param)
								}
								GenericParam::Const(mut param) => {
									param.attrs = VersionedAttr::process_attrs_for_version(
										param.attrs,
										version,
										Some(args),
										errs,
									);

									GenericParam::Const(param)
								}
							}
						})
					})
					.collect()
			},
		);

		let (ver_where, ()) = &input.helpers;

		let mut where_clause = input
			.where_clause
			.iter()
			.flat_map(|wc| wc.predicates.iter().cloned())
			.chain(
				ver_where
					.iter()
					.flat_map(|VersionedWhere(versions, preds)| {
						versions.contains(version).then(|| preds.0.iter().cloned())
					})
					.flatten(),
			)
			.collect();

		// pseudo-macros can occur in generic parameters in traits here
		let mut epm = ExpandPseudoMacros {
			version,
			validate_args: Some(args),
			errs,
		};
		for param in generic_params.iter_mut().flatten() {
			epm.visit_generic_param_mut(param);
		}
		for pred in &mut where_clause {
			epm.visit_where_predicate_mut(pred);
		}

		let ident = args
			.rename
			.get(&crate::parse::args::RenameSource::Version(version))
			.cloned()
			.unwrap_or_else(|| versioned_ident(&input.ident, version));

		let res = OutputItemBase {
			attrs: VersionedAttr::process_attrs_for_version(
				input.attrs.clone(),
				version,
				Some(args),
				errs,
			),
			vis: input.vis.clone(),
			ident,
			generic_params,
			where_clause,
		};
		(version, res)
	})
}

fn versioned_fields(
	version: usize,
	input: &ParsedFields,
	args: &Args,
	errs: &mut ErrorSink,
) -> OutputFields {
	let fields = input
		.fields
		.iter()
		.filter_map(|(helpers, field)| {
			let (versions, ()) = helpers;
			let is_included =
				versions.map_or(true, |VersionFilter(versions)| versions.contains(version));

			is_included.then(|| {
				let mut field = field.clone();

				field.attrs = VersionedAttr::process_attrs_for_version(
					field.attrs,
					version,
					Some(args),
					errs,
				);

				// MaybeTODO: Let e.g. `ver_match!` contribute its match arms towards the highest mentioned version.
				// -> Idea: Split peudo-macros into two phases:
				//    First, before version resolution, parse the args and store the parsed values in a global hashmap.
				//    Then replace the input with the key so that the second phase can effortlessly reuse the result.
				ExpandPseudoMacros {
					version,
					validate_args: Some(args),
					errs,
				}
				.visit_field_mut(&mut field);

				field
			})
		})
		.collect();

	OutputFields {
		fields,
		flavor_bias: input.flavor_bias,
	}
}