typeshare_engine/
driver.rs

1use std::{collections::HashMap, io};
2
3use anyhow::Context as _;
4use clap::{CommandFactory as _, FromArgMatches as _};
5use clap_complete::generate as generate_completions;
6use ignore::{overrides::OverrideBuilder, types::TypesBuilder, WalkBuilder};
7use itertools::Itertools;
8use lazy_format::lazy_format;
9use typeshare_model::prelude::{CrateName, FilesMode, Language};
10
11use crate::{
12    args::{add_lang_argument, add_language_params_to_clap, Command, OutputLocation, StandardArgs},
13    config::{
14        self, compute_args_set, load_config, load_language_config_from_file_and_args, CliArgsSet,
15    },
16    parser::{parse_input, parser_inputs, ParsedData},
17    writer::write_output,
18};
19
20pub struct PersonalizeClap {
21    pub name: &'static str,
22    pub version: &'static str,
23    pub author: &'static str,
24    pub about: &'static str,
25}
26
27pub trait LanguageSet<'config> {
28    type LanguageMetas: 'static;
29
30    /// Each language has a set of configuration metadata, describing all
31    /// of its configuration parameters. This metadata is used to populate
32    /// the clap command with language specific parameters for each language,
33    /// and to load a fully configured language. It is computed based on the
34    /// serde serialization of a config.
35    fn compute_language_metas() -> anyhow::Result<Self::LanguageMetas>;
36
37    /// Add the `--language` argument to the command, such that all of the
38    /// languages in this set are possible values for that argument
39    fn add_lang_argument(command: clap::Command) -> clap::Command;
40
41    /// Add all of the language-specific arguments to the clap command.
42    fn add_language_specific_arguments(
43        command: clap::Command,
44        metas: &Self::LanguageMetas,
45    ) -> clap::Command;
46
47    fn execute_typeshare_for_language(
48        language: &str,
49        config: &'config config::Config,
50        args: &'config clap::ArgMatches,
51        metas: &Self::LanguageMetas,
52        data: HashMap<Option<CrateName>, ParsedData>,
53        destination: &OutputLocation<'_>,
54    ) -> anyhow::Result<()>;
55}
56
57macro_rules! metas {
58    ([$($CliArgsSet:ident)*]) => {
59        ($($CliArgsSet,)*)
60    };
61
62    ([$($CliArgsSet:ident)*] $Language:ident $($Tail:ident)*) => {
63        metas! {[$($CliArgsSet)* CliArgsSet] $($Tail)*}
64    };
65}
66
67macro_rules! language_set_for {
68    ($Language:ident $($Tail:ident)*) => {
69        language_set_for! {
70            [$Language] $($Tail)*
71        }
72    };
73
74    ([$($Language:ident)*] $Head:ident $($Tail:ident)*) => {
75        language_set_for! {[$($Language)*]}
76
77        language_set_for! {
78            [$($Language)* $Head] $($Tail)*
79        }
80    };
81
82    ([$($Language:ident)+]) => {
83        impl<'config, $($Language,)*> LanguageSet<'config> for ($($Language,)*)
84            where $(
85                $Language: Language<'config>,
86            )*
87        {
88            type LanguageMetas = metas!([] $($Language)*);
89
90            fn compute_language_metas() -> anyhow::Result<Self::LanguageMetas> {
91                Ok(
92                     ($(
93                        compute_args_set::<$Language>()?,
94                    )*),
95              )
96            }
97
98            fn add_lang_argument(command: clap::Command)->clap::Command {
99                add_lang_argument(command, &[$(<$Language as Language>::NAME,)*])
100            }
101
102            fn add_language_specific_arguments(
103                command: clap::Command,
104                metas: &Self::LanguageMetas,
105            ) -> clap::Command {
106                #[allow(non_snake_case)]
107                let ($($Language,)*) = metas;
108
109                $(
110                    let command = add_language_params_to_clap(
111                        command,
112                        <$Language as Language>::NAME,
113                        $Language
114                    );
115                )*
116
117                command
118            }
119
120            fn execute_typeshare_for_language(
121                language: &str,
122                config: &'config config::Config,
123                args: &'config clap::ArgMatches,
124                metas: &Self::LanguageMetas,
125                data: HashMap<Option<CrateName>, ParsedData>,
126                destination: &OutputLocation<'_>,
127            ) -> anyhow::Result<()> {
128                #[allow(non_snake_case)]
129                let ($($Language,)*) = metas;
130
131                $(
132                    if language == <$Language as Language>::NAME {
133                        execute_typeshare_for_language::<$Language>(
134                            config,
135                            args,
136                            $Language,
137                            data,
138                            destination
139                        )
140                    } else
141                )*
142                {
143                    anyhow::bail!("{language} isn't a valid language; clap should have prevented this")
144                }
145            }
146        }
147    }
148}
149
150fn execute_typeshare_for_language<'config, 'a, L: Language<'config>>(
151    config: &'config config::Config,
152    args: &'config clap::ArgMatches,
153    meta: &'a CliArgsSet,
154    data: HashMap<Option<CrateName>, ParsedData>,
155    destination: &OutputLocation<'_>,
156) -> anyhow::Result<()> {
157    let name = L::NAME;
158
159    let config = load_language_config_from_file_and_args::<L>(&config, &args, meta)
160        .with_context(|| format!("failed to load configuration for language {name}"))?;
161
162    let language_implementation = <L>::new_from_config(config)
163        .with_context(|| format!("failed to load configuration for language {name}"))?;
164
165    write_output(&language_implementation, data, destination)
166        .with_context(|| format!("failed to generate typeshared code for language {name}"))?;
167
168    Ok(())
169}
170
171// We support typeshare binaries for up to 16 languages. Fork us and make your
172// own if that's not enough for you.
173language_set_for! {
174    A B C D
175    E F G H
176    I J K L
177    M N O P
178}
179
180/// This trait is used by the driver macro to unify the 'config lifetime
181/// across all of the language types. I'm open to suggesstions for getting
182/// rid of this.
183pub trait LanguageHelper {
184    type LanguageSet<'config>: LanguageSet<'config>;
185}
186
187pub fn main_body<Helper>() -> anyhow::Result<()>
188where
189    Helper: LanguageHelper,
190{
191    let language_metas = Helper::LanguageSet::compute_language_metas()?;
192    let command = StandardArgs::command();
193
194    // let command = command
195    //     .name(personalize.name)
196    //     .version(personalize.version)
197    //     .author(personalize.author)
198    //     .about(personalize.about);
199
200    let command = Helper::LanguageSet::add_lang_argument(command);
201    let command = Helper::LanguageSet::add_language_specific_arguments(command, &language_metas);
202
203    // Parse command line arguments. Need to clone here because we
204    // need to be able to generate completions later.
205    let args = command.clone().get_matches();
206
207    // Load the standard arguments from the parsed arguments. Generally
208    // we expect that this won't fail, because the `command` has been
209    // configured to only give us valid arrangements of args
210    let standard_args = StandardArgs::from_arg_matches(&args)
211        .expect("StandardArgs should always be loadable from a `command`");
212
213    // If we asked for completions, do that before anything else
214    if let Some(options) = standard_args.subcommand {
215        match options {
216            Command::Completions { shell } => {
217                let mut command = command;
218                let bin_name = command.get_name().to_string();
219                generate_completions(shell, &mut command, bin_name, &mut io::stdout());
220            }
221        }
222
223        return Ok(());
224    }
225
226    // Load all of the language configurations
227    let config = load_config(standard_args.config.as_deref())?;
228
229    let target_os = standard_args
230        .target_os
231        .as_ref()
232        .or_else(|| config.global_config().target_os.as_ref())
233        .map(|targets| targets.iter().map(|target| target.as_str()).collect_vec());
234
235    eprintln!("TARGET {target_os:?}");
236
237    // Construct the directory walker that will produce the list of
238    // files to typeshare
239    let walker = {
240        let directories = standard_args.directories.as_slice();
241        let (first_dir, other_dirs) = directories
242            .split_first()
243            .expect("clap should guarantee that there's at least one input directory");
244
245        let mut types = TypesBuilder::new();
246        types.add("rust", "*.rs").unwrap();
247        types.select("rust");
248
249        let mut overrides = OverrideBuilder::new("");
250        // We need this global match because an override, by default, rejects
251        // files. We need to *accept* all files, *except* those that are
252        // explicitly rejected by the subsequent lines.
253        overrides.add("**/*.rs").unwrap();
254        overrides.add("!**/tests/**").unwrap();
255        overrides.add("!**/examples/**").unwrap();
256        overrides.add("!**/benches/**").unwrap();
257        overrides.add("!build.rs").unwrap();
258        overrides.add("**/src/**").unwrap();
259        let overrides = overrides.build().unwrap();
260
261        let mut walker_builder = WalkBuilder::new(first_dir);
262        walker_builder.types(types.build().unwrap());
263        walker_builder.overrides(overrides);
264        other_dirs.iter().for_each(|dir| {
265            walker_builder.add(dir);
266        });
267        walker_builder.build()
268    };
269
270    // Collect all of the files we intend to parse with typeshare
271    let parser_inputs = parser_inputs(walker);
272
273    // Parse those files
274    let data = parse_input(
275        parser_inputs,
276        &[],
277        if standard_args.output.file.is_some() {
278            FilesMode::Single
279        } else {
280            FilesMode::Multi(())
281        },
282        target_os.as_deref(),
283    )
284    .map_err(|errors| {
285        // TODO: switch to miette
286        let errors = &errors;
287        let message = lazy_format!("{error}\n" for error in errors);
288        anyhow::anyhow!("{message}")
289    })
290    .context("error parsing input files")?;
291
292    let destination = standard_args.output.location();
293
294    let language: &String = args
295        .get_one("language")
296        .expect("clap should guarantee that --lang is provided");
297
298    Helper::LanguageSet::execute_typeshare_for_language(
299        &language,
300        &config,
301        &args,
302        &language_metas,
303        data,
304        &destination,
305    )
306}