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 #[clap(short = 'i', long)]
19 input_dir: PathBuf,
20
21 #[clap(short = 'o', long)]
23 output: PathBuf,
24
25 #[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}