Skip to main content

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