generate_code/
main.rs

1#![allow(clippy::only_used_in_recursion)]
2
3use std::path::{Path, PathBuf};
4
5use anyhow::{bail, Context, Result};
6use clap::Parser;
7use swc_config::regex::CachedRegex;
8use syn::Item;
9
10use crate::types::qualify_types;
11
12mod generators;
13mod types;
14
15#[derive(Debug, Parser)]
16struct CliArgs {
17    /// The directory containing the crate to generate the visitor for.
18    #[clap(short = 'i', long)]
19    input_dir: PathBuf,
20
21    /// The file for the generated visitor code.
22    #[clap(short = 'o', long)]
23    output: PathBuf,
24
25    /// The list of types to exclude from the generated visitor.
26    #[clap(long)]
27    exclude: Vec<String>,
28}
29
30fn main() -> Result<()> {
31    let CliArgs {
32        input_dir,
33        output,
34        exclude,
35    } = CliArgs::parse();
36
37    run_visitor_codegen(&input_dir, &output, &exclude)?;
38
39    Ok(())
40}
41
42fn run_visitor_codegen(input_dir: &Path, output: &Path, excluded_types: &[String]) -> Result<()> {
43    let crate_name = input_dir.file_name().unwrap().to_str().unwrap();
44
45    let input_dir = input_dir
46        .canonicalize()
47        .context("faield to canonicalize input directory")?
48        .join("src");
49
50    eprintln!("Generating visitor for crate in directory: {input_dir:?}");
51    let input_files = collect_input_files(&input_dir)?;
52    eprintln!("Found {} input files", input_files.len());
53
54    eprintln!("Generating visitor in directory: {output:?}");
55
56    let inputs = input_files
57        .iter()
58        .map(|file| {
59            parse_rust_file(file).with_context(|| format!("failed to parse file: {file:?}"))
60        })
61        .map(|res| res.map(qualify_types))
62        .collect::<Result<Vec<_>>>()?;
63
64    let mut all_type_defs = inputs.iter().flat_map(get_type_defs).collect::<Vec<_>>();
65    all_type_defs.retain(|type_def| {
66        let ident = match type_def {
67            Item::Struct(data) => &data.ident,
68            Item::Enum(data) => &data.ident,
69            _ => return false,
70        };
71
72        for type_name in excluded_types {
73            let regex = CachedRegex::new(type_name).expect("failed to create regex");
74            if regex.is_match(&ident.to_string()) {
75                return false;
76            }
77        }
78
79        true
80    });
81
82    all_type_defs.sort_by_key(|item| match item {
83        Item::Enum(data) => Some(data.ident.clone()),
84        Item::Struct(data) => Some(data.ident.clone()),
85        _ => None,
86    });
87
88    let file = generators::visitor::generate(crate_name, &all_type_defs, excluded_types);
89
90    let output_content = quote::quote!(#file).to_string();
91
92    let original = std::fs::read_to_string(output).ok();
93
94    std::fs::write(output, output_content).context("failed to write the output file")?;
95
96    eprintln!("Generated visitor code in file: {output:?}");
97
98    run_cargo_fmt(output)?;
99
100    if std::env::var("CI").is_ok_and(|v| v != "1") {
101        if let Some(original) = original {
102            let output =
103                std::fs::read_to_string(output).context("failed to read the output file")?;
104
105            if original != output {
106                bail!(
107                    "The generated code is not up to date. Please run `cargo codegen` and commit \
108                     the changes."
109                );
110            }
111        }
112    }
113
114    Ok(())
115}
116
117#[test]
118fn test_ecmascript() {
119    run_visitor_codegen(
120        Path::new("../../crates/swc_ecma_ast"),
121        Path::new("../../crates/swc_ecma_visit/src/generated.rs"),
122        &[
123            "Align64".into(),
124            "EncodeBigInt".into(),
125            "EsVersion".into(),
126            "FnPass".into(),
127        ],
128    )
129    .unwrap();
130}
131
132#[test]
133fn test_ecmascript_regexp() {
134    run_visitor_codegen(
135        Path::new("../../crates/swc_ecma_regexp_ast"),
136        Path::new("../../crates/swc_ecma_regexp_visit/src/generated.rs"),
137        &["Options".into()],
138    )
139    .unwrap();
140}
141
142#[test]
143fn test_css() {
144    run_visitor_codegen(
145        Path::new("../../crates/swc_css_ast"),
146        Path::new("../../crates/swc_css_visit/src/generated.rs"),
147        &[],
148    )
149    .unwrap();
150}
151
152#[test]
153fn test_html() {
154    run_visitor_codegen(
155        Path::new("../../crates/swc_html_ast"),
156        Path::new("../../crates/swc_html_visit/src/generated.rs"),
157        &[],
158    )
159    .unwrap();
160}
161
162#[test]
163fn test_xml() {
164    run_visitor_codegen(
165        Path::new("../../crates/swc_xml_ast"),
166        Path::new("../../crates/swc_xml_visit/src/generated.rs"),
167        &[],
168    )
169    .unwrap();
170}
171
172fn get_type_defs(file: &syn::File) -> Vec<&Item> {
173    let mut type_defs = Vec::new();
174    for item in &file.items {
175        match item {
176            Item::Struct(_) | Item::Enum(_) => {
177                type_defs.push(item);
178            }
179
180            _ => {}
181        }
182    }
183    type_defs
184}
185
186fn parse_rust_file(file: &Path) -> Result<syn::File> {
187    let content = std::fs::read_to_string(file).context("failed to read the input file")?;
188    let syntax = syn::parse_file(&content).context("failed to parse the input file using syn")?;
189    Ok(syntax)
190}
191
192fn collect_input_files(input_dir: &Path) -> Result<Vec<PathBuf>> {
193    Ok(walkdir::WalkDir::new(input_dir)
194        .into_iter()
195        .filter_map(|entry| entry.ok())
196        .filter(|entry| entry.file_type().is_file())
197        .map(|entry| entry.path().to_path_buf())
198        .collect())
199}
200
201fn run_cargo_fmt(file: &Path) -> Result<()> {
202    let file = file.canonicalize().context("failed to canonicalize file")?;
203
204    let mut cmd = std::process::Command::new("cargo");
205    cmd.arg("fmt").arg("--").arg(file);
206
207    eprintln!("Running: {cmd:?}");
208    let status = cmd.status().context("failed to run cargo fmt")?;
209
210    if !status.success() {
211        bail!("cargo fmt failed with status: {:?}", status);
212    }
213
214    Ok(())
215}