typeshare_engine/
writer.rs

1use std::{
2    collections::{BTreeMap, BTreeSet, HashMap, HashSet},
3    fs,
4    path::{Path, PathBuf},
5};
6
7use anyhow::Context;
8use itertools::Itertools;
9use typeshare_model::prelude::*;
10
11use crate::{args::OutputLocation, parser::ParsedData, topsort::topsort};
12
13pub fn write_output<'c>(
14    lang: &impl Language<'c>,
15    crate_parsed_data: HashMap<Option<CrateName>, ParsedData>,
16    dest: &OutputLocation<'_>,
17) -> anyhow::Result<()> {
18    match dest {
19        OutputLocation::File(file) => {
20            // merge all data together
21            let mut parsed_data = crate_parsed_data
22                .into_values()
23                .reduce(|mut data, new_data| {
24                    data.merge(new_data);
25                    data
26                })
27                .context("called `write_output` with no data")?;
28
29            parsed_data.sort_contents();
30            write_single_file(lang, file, &parsed_data)
31        }
32        OutputLocation::Folder(directory) => {
33            // TODO: compute import candidates here
34
35            let crate_parsed_data = crate_parsed_data
36                .into_iter()
37                .map(|(crate_name, mut data)| match crate_name {
38                    Some(crate_name) => {
39                        data.sort_contents();
40                        Ok((crate_name, data))
41                    }
42                    None => anyhow::bail!(
43                        "got files with unknown crates; all files \
44                         must be in crates in multi-file mode"
45                    ),
46                })
47                .try_collect()?;
48
49            write_multiple_files(lang, directory, &crate_parsed_data)
50        }
51    }
52}
53
54/// Write multiple module files.
55pub fn write_multiple_files<'c>(
56    lang: &impl Language<'c>,
57    output_folder: &Path,
58    crate_parsed_data: &HashMap<CrateName, ParsedData>,
59) -> anyhow::Result<()> {
60    let mut output_files = Vec::with_capacity(crate_parsed_data.len());
61
62    for (crate_name, parsed_data) in crate_parsed_data {
63        let file_path = output_folder.join(&lang.output_filename_for_crate(&crate_name));
64
65        let mut output = Vec::new();
66
67        generate_types(
68            lang,
69            &mut output,
70            parsed_data,
71            FilesMode::Multi(&crate_name),
72        )
73        .with_context(|| format!("error generating typeshare types for crate {crate_name}"))?;
74
75        check_write_file(&file_path, output).with_context(|| {
76            format!(
77                "error writing generated typeshare types for crate {crate_name} to '{}'",
78                file_path.display()
79            )
80        })?;
81
82        output_files.push((crate_name, file_path));
83    }
84
85    output_files.sort_by_key(|&(crate_name, _)| crate_name);
86
87    lang.write_additional_files(
88        output_folder,
89        output_files
90            .iter()
91            .map(|(crate_name, file_path)| (*crate_name, file_path.as_path())),
92    )
93    .context("failed to write additional files")?;
94
95    Ok(())
96}
97
98/// Write all types to a single file.
99pub fn write_single_file<'c>(
100    lang: &impl Language<'c>,
101    file_name: &Path,
102    parsed_data: &ParsedData,
103) -> Result<(), anyhow::Error> {
104    let mut output = Vec::new();
105
106    generate_types(lang, &mut output, parsed_data, FilesMode::Single)
107        .context("error generating typeshare types")?;
108
109    let outfile = Path::new(file_name).to_path_buf();
110    check_write_file(&outfile, output)
111        .context("error writing generated typeshare types to file")?;
112    Ok(())
113}
114
115/// Write the file if the contents have changed.
116fn check_write_file(outfile: &PathBuf, output: Vec<u8>) -> anyhow::Result<()> {
117    match fs::read(outfile) {
118        Ok(buf) if buf == output => {
119            // avoid writing the file to leave the mtime intact
120            // for tools which might use it to know when to
121            // rebuild.
122            eprintln!("Skipping writing to {outfile:?} no changes");
123            return Ok(());
124        }
125        _ => {}
126    }
127
128    if !output.is_empty() {
129        let out_dir = outfile
130            .parent()
131            .context(format!("Could not get parent for {outfile:?}"))?;
132        // If the output directory doesn't already exist, create it.
133        if !out_dir.exists() {
134            fs::create_dir_all(out_dir).context("failed to create output directory")?;
135        }
136
137        fs::write(outfile, output).context("failed to write output")?;
138    }
139    Ok(())
140}
141
142/// An enum that encapsulates units of code generation for Typeshare.
143/// Analogous to `syn::Item`, even though our variants are more limited.
144#[non_exhaustive]
145#[derive(Debug, Clone, Copy, PartialEq)]
146pub enum BorrowedRustItem<'a> {
147    /// A `struct` definition
148    Struct(&'a RustStruct),
149    /// An `enum` definition
150    Enum(&'a RustEnum),
151    /// A `type` definition or newtype struct.
152    Alias(&'a RustTypeAlias),
153    /// A `const` definition
154    Const(&'a RustConst),
155}
156
157impl BorrowedRustItem<'_> {
158    pub fn name(&self) -> &str {
159        match *self {
160            BorrowedRustItem::Struct(item) => &item.id,
161            BorrowedRustItem::Enum(item) => &item.shared().id,
162            BorrowedRustItem::Alias(item) => &item.id,
163            BorrowedRustItem::Const(item) => &item.id,
164        }
165        .original
166        .as_str()
167    }
168}
169
170/// Given `data`, generate type-code for this language and write it out to `writable`.
171/// Returns whether or not writing was successful.
172fn generate_types<'c>(
173    lang: &impl Language<'c>,
174    out: &mut Vec<u8>,
175    data: &ParsedData,
176    mode: FilesMode<&CrateName>,
177) -> anyhow::Result<()> {
178    lang.begin_file(out, mode)
179        .context("error writing file header")?;
180
181    if let FilesMode::Multi(crate_name) = mode {
182        let all_types = HashMap::new();
183        lang.write_imports(out, crate_name, used_imports(&data, crate_name, &all_types))
184            .context("error writing imports")?;
185    }
186
187    let ParsedData {
188        structs,
189        enums,
190        aliases,
191        consts,
192        ..
193    } = data;
194
195    let mut items = Vec::from_iter(
196        aliases
197            .iter()
198            .map(BorrowedRustItem::Alias)
199            .chain(structs.iter().map(BorrowedRustItem::Struct))
200            .chain(enums.iter().map(BorrowedRustItem::Enum))
201            .chain(consts.iter().map(BorrowedRustItem::Const)),
202    );
203
204    topsort(&mut items);
205
206    for thing in &items {
207        let name = thing.name();
208
209        match thing {
210            BorrowedRustItem::Enum(e) => lang
211                .write_enum(out, e)
212                .with_context(|| format!("error writing enum {name}"))?,
213            BorrowedRustItem::Struct(s) => lang
214                .write_struct(out, s)
215                .with_context(|| format!("error writing struct {name}"))?,
216            BorrowedRustItem::Alias(a) => lang
217                .write_type_alias(out, a)
218                .with_context(|| format!("error writing type alias {name}"))?,
219            BorrowedRustItem::Const(c) => lang
220                .write_const(out, c)
221                .with_context(|| format!("error writing const {name}"))?,
222        }
223    }
224
225    lang.end_file(out, mode)
226        .context("error writing file trailer")
227}
228
229/// Lookup any refeferences to other typeshared types in order to build
230/// a list of imports for the generated module.
231fn used_imports<'a, 'b: 'a>(
232    data: &'b ParsedData,
233    crate_name: &CrateName,
234    all_types: &'a HashMap<CrateName, HashSet<TypeName>>,
235) -> BTreeMap<&'a CrateName, BTreeSet<&'a TypeName>> {
236    let mut used_imports: BTreeMap<&'a CrateName, BTreeSet<&'a TypeName>> = BTreeMap::new();
237
238    // If we have reference that is a re-export we can attempt to find it with the
239    // following heuristic.
240    let fallback = |referenced_import: &'a ImportedType,
241                    used: &mut BTreeMap<&'a CrateName, BTreeSet<&'a TypeName>>| {
242        // Find the first type that does not belong to the current crate.
243        if let Some((crate_name, ty)) = all_types
244            .iter()
245            .flat_map(|(k, v)| {
246                v.iter()
247                    .find(|&t| *t == referenced_import.type_name && k != crate_name)
248                    .map(|t| (k, t))
249            })
250            .next()
251        {
252            println!("Warning: Using {crate_name} as module for {ty} which is not in referenced crate {}", referenced_import.base_crate);
253            used.entry(crate_name).or_default().insert(ty);
254        } else {
255            // println!("Could not lookup reference {referenced_import:?}");
256        }
257    };
258
259    for referenced_import in data
260        .import_types
261        .iter()
262        // Skip over imports that reference the current crate. They
263        // are all collapsed into one module per crate.
264        .filter(|imp| imp.base_crate != *crate_name)
265    {
266        // Look up the types for the referenced imported crate.
267        if let Some(type_names) = all_types.get(&referenced_import.base_crate) {
268            if referenced_import.type_name == "*" {
269                // We can have "*" wildcard here. We need to add all.
270                used_imports
271                    .entry(&referenced_import.base_crate)
272                    .and_modify(|names| names.extend(type_names.iter()));
273            } else if let Some(ty_name) = type_names.get(&referenced_import.type_name) {
274                // Add referenced import for each matching type.
275                used_imports
276                    .entry(&referenced_import.base_crate)
277                    .or_default()
278                    .insert(ty_name);
279            } else {
280                fallback(referenced_import, &mut used_imports);
281            }
282        } else {
283            // We might have a re-export from another crate.
284            fallback(referenced_import, &mut used_imports);
285        }
286    }
287    used_imports
288}