Skip to content

Commit 0bbbfbb

Browse files
committed
Add a re-export check to verify
Add a new check to verify that ensures all types from the `types` crate are re-exported for every version. This includes any auxiliary types from fields in the RPC structs.
1 parent 15ba220 commit 0bbbfbb

File tree

4 files changed

+360
-1
lines changed

4 files changed

+360
-1
lines changed

verify/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,5 @@ edition = "2021"
77
anyhow = "1.0.93"
88
clap = { version = "4.5.23", features = ["cargo"] }
99
regex = "1"
10+
syn = { version = "2.0", default-features = false, features = ["full", "parsing"] }
11+
walkdir = "2.5"

verify/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
55
pub mod method;
66
pub mod model;
7+
pub mod reexports;
78
pub mod ssot;
89
pub mod versioned;
910

verify/src/main.rs

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,15 @@
88
//! - That an expected return type is provided if the method is supported.
99
//! - That there is a `model` type if required.
1010
//! - That the method has an integration test.
11+
//! - That re-exports in `corepc-types` are complete.
1112
1213
use std::process;
1314

1415
use anyhow::Result;
1516
use clap::{arg, Command};
1617
use verify::method::{Method, Return};
1718
use verify::versioned::{self, Status};
18-
use verify::{method, model, ssot, Version};
19+
use verify::{method, model, reexports, ssot, Version};
1920

2021
// TODO: Enable running from any directory, currently errors if run from `src/`.
2122
// TODO: Add a --quiet option.
@@ -134,6 +135,19 @@ fn verify_version(version: Version, test_output: Option<&String>, quiet: bool) -
134135
}
135136
}
136137

138+
let msg = "Checking that corepc-types re-exports are complete";
139+
check(msg, quiet);
140+
match reexports::check_type_reexports(version) {
141+
Ok(()) => close(true, quiet),
142+
Err(e) => {
143+
if !quiet {
144+
eprintln!("{}", e);
145+
}
146+
close(false, quiet);
147+
failures += 1;
148+
}
149+
}
150+
137151
if failures > 0 {
138152
return Err(anyhow::anyhow!("verification failed ({} check(s) failed)", failures));
139153
}

verify/src/reexports.rs

Lines changed: 342 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,342 @@
1+
// SPDX-License-Identifier: CC0-1.0
2+
3+
//! Checks the re-exports in `corepc-types` are complete.
4+
5+
use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
6+
use std::fs;
7+
use std::path::{Path, PathBuf};
8+
9+
use anyhow::{anyhow, Context, Result};
10+
use syn::{Fields, GenericArgument, Item, PathArguments, Type, UseTree, Visibility};
11+
use walkdir::WalkDir;
12+
13+
use crate::Version;
14+
15+
type VersionedDeps = HashMap<String, BTreeMap<String, BTreeSet<String>>>;
16+
type ParsedTypeFiles = (Vec<(String, PathBuf)>, HashSet<String>);
17+
18+
/// The original version/type behind a public re-export.
19+
#[derive(Clone, Debug)]
20+
struct ExportInfo {
21+
source_version: String,
22+
source_ident: String,
23+
exported_ident: String,
24+
}
25+
26+
/// A flattened path entry gathered from a `use` tree.
27+
#[derive(Debug)]
28+
struct UseEntry {
29+
path: Vec<String>,
30+
rename: Option<String>,
31+
}
32+
33+
/// Checks that every type is re-exported for the requested version.
34+
pub fn check_type_reexports(version: Version) -> Result<()> {
35+
let crate_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
36+
let src_dir = crate_dir.join("../types/src");
37+
let all_versions = collect_version_dirs(&src_dir)?;
38+
let (files, known_names) = collect_type_files_and_names(&src_dir, &all_versions)?;
39+
let definitions = collect_type_definitions(&files, &known_names)?;
40+
let version_name = version.to_string();
41+
let export_map = collect_exports(&src_dir, &version_name)?;
42+
43+
let mut missing = Vec::new();
44+
45+
// Checks every type defined in this version is publicly re-exported.
46+
let version_defs = match definitions.get(&version_name) {
47+
Some(defs) => defs,
48+
None => {
49+
let msg = format!("no definitions found for version {}", version_name);
50+
return Err(anyhow::anyhow!(msg));
51+
}
52+
};
53+
54+
for type_name in version_defs.keys() {
55+
let exported = export_map.values().any(|info| {
56+
info.source_version == version_name && type_name == &info.source_ident
57+
});
58+
if !exported {
59+
missing.push(format!(
60+
"{} defines {} but does not re-export it",
61+
version_name, type_name
62+
));
63+
}
64+
}
65+
66+
// Checks all auxiliary types are re-exported.
67+
for (exported_name, export) in &export_map {
68+
if let Some(deps) = definitions
69+
.get(&export.source_version)
70+
.and_then(|map| map.get(&export.source_ident))
71+
{
72+
for dep in deps {
73+
if !export_map.contains_key(dep) {
74+
missing.push(format!(
75+
"{} re-exports {} from {} but does not re-export auxiliary type {}",
76+
version_name, exported_name, export.source_version, dep
77+
));
78+
}
79+
}
80+
}
81+
}
82+
83+
if missing.is_empty() {
84+
return Ok(());
85+
}
86+
let msg = format!("Missing re-exports:\n{}", missing.join("\n"));
87+
Err(anyhow!(msg))
88+
}
89+
90+
/// Returns all the types version root directories `types/src/vXX`.
91+
fn collect_version_dirs(src_dir: &Path) -> Result<Vec<String>> {
92+
let mut versions = Vec::new();
93+
for entry in fs::read_dir(src_dir)
94+
.with_context(|| format!("reading version directory listing in {}", src_dir.display()))?
95+
{
96+
let entry = entry?;
97+
if !entry.file_type()?.is_dir() {
98+
continue;
99+
}
100+
let name = entry.file_name();
101+
let name = name.to_string_lossy();
102+
if is_version_dir_name(&name) {
103+
versions.push(name.into_owned());
104+
}
105+
}
106+
versions.sort();
107+
Ok(versions)
108+
}
109+
110+
/// Parses all versioned source files and records every public struct/enum name.
111+
fn collect_type_files_and_names(
112+
src_dir: &Path,
113+
versions: &[String],
114+
) -> Result<ParsedTypeFiles> {
115+
let mut files = Vec::new();
116+
let mut names = HashSet::new();
117+
118+
for version in versions {
119+
let dir = src_dir.join(version);
120+
for entry in WalkDir::new(&dir).into_iter().filter_map(Result::ok) {
121+
if !entry.file_type().is_file() {
122+
continue;
123+
}
124+
if entry.path().extension().and_then(|ext| ext.to_str()) != Some("rs") {
125+
continue;
126+
}
127+
let content = fs::read_to_string(entry.path())
128+
.with_context(|| format!("reading source file {}", entry.path().display()))?;
129+
let syntax = syn::parse_file(&content)
130+
.with_context(|| format!("parsing source file {}", entry.path().display()))?;
131+
for item in &syntax.items {
132+
match item {
133+
Item::Struct(item_struct) if is_public(&item_struct.vis) => {
134+
names.insert(item_struct.ident.to_string());
135+
}
136+
Item::Enum(item_enum) if is_public(&item_enum.vis) => {
137+
names.insert(item_enum.ident.to_string());
138+
}
139+
_ => {}
140+
}
141+
}
142+
files.push((version.clone(), entry.into_path()));
143+
}
144+
}
145+
146+
Ok((files, names))
147+
}
148+
149+
/// Builds a per-version dependency map for every public type.
150+
fn collect_type_definitions(
151+
files: &[(String, PathBuf)],
152+
known_names: &HashSet<String>,
153+
) -> Result<VersionedDeps> {
154+
let mut defs: VersionedDeps = HashMap::new();
155+
156+
for (version, path) in files {
157+
let content = fs::read_to_string(path)
158+
.with_context(|| format!("reading source file {}", path.display()))?;
159+
let syntax = syn::parse_file(&content)
160+
.with_context(|| format!("parsing source file {}", path.display()))?;
161+
for item in syntax.items {
162+
match item {
163+
Item::Struct(item_struct) if is_public(&item_struct.vis) => {
164+
let deps = collect_deps_from_fields(&item_struct.fields, known_names);
165+
defs.entry(version.clone()).or_default().insert(item_struct.ident.to_string(), deps);
166+
}
167+
Item::Enum(item_enum) if is_public(&item_enum.vis) => {
168+
let mut deps = BTreeSet::new();
169+
for variant in item_enum.variants {
170+
deps.extend(collect_deps_from_fields(&variant.fields, known_names));
171+
}
172+
defs.entry(version.clone()).or_default().insert(item_enum.ident.to_string(), deps);
173+
}
174+
_ => {}
175+
}
176+
}
177+
}
178+
179+
Ok(defs)
180+
}
181+
182+
/// Reads `mod.rs` for the chosen version and lists its public re-exports.
183+
fn collect_exports(
184+
src_dir: &Path,
185+
version: &str,
186+
) -> Result<HashMap<String, ExportInfo>> {
187+
let mod_path = src_dir.join(version).join("mod.rs");
188+
let content = fs::read_to_string(&mod_path)
189+
.with_context(|| format!("reading {}", mod_path.display()))?;
190+
let syntax = syn::parse_file(&content)
191+
.with_context(|| format!("parsing {}", mod_path.display()))?;
192+
let mut exports = HashMap::new();
193+
194+
for item in syntax.items {
195+
if let Item::Use(item_use) = item {
196+
if !is_public(&item_use.vis) {
197+
continue;
198+
}
199+
let mut entries = Vec::new();
200+
flatten_use_tree(Vec::new(), &item_use.tree, &mut entries);
201+
for entry in entries {
202+
if let Some(info) = interpret_flat_use(version, &entry) {
203+
exports.insert(info.exported_ident.clone(), info);
204+
}
205+
}
206+
}
207+
}
208+
209+
Ok(exports)
210+
}
211+
212+
/// Extracts referenced auxiliary types from the provided field set.
213+
fn collect_deps_from_fields(fields: &Fields, known_names: &HashSet<String>) -> BTreeSet<String> {
214+
let mut deps = BTreeSet::new();
215+
match fields {
216+
Fields::Named(named) => {
217+
for field in &named.named {
218+
collect_type_dependencies(&field.ty, known_names, &mut deps);
219+
}
220+
}
221+
Fields::Unnamed(unnamed) => {
222+
for field in &unnamed.unnamed {
223+
collect_type_dependencies(&field.ty, known_names, &mut deps);
224+
}
225+
}
226+
Fields::Unit => {}
227+
}
228+
deps
229+
}
230+
231+
/// Recursively walks a type expression to find referenced auxiliary types.
232+
fn collect_type_dependencies(
233+
ty: &Type,
234+
known_names: &HashSet<String>,
235+
deps: &mut BTreeSet<String>,
236+
) {
237+
match ty {
238+
Type::Path(type_path) => {
239+
if let Some(segment) = type_path.path.segments.last() {
240+
let ident = segment.ident.to_string();
241+
if known_names.contains(&ident) {
242+
deps.insert(ident);
243+
}
244+
}
245+
for segment in &type_path.path.segments {
246+
if let PathArguments::AngleBracketed(args) = &segment.arguments {
247+
for arg in &args.args {
248+
if let GenericArgument::Type(inner) = arg {
249+
collect_type_dependencies(inner, known_names, deps);
250+
}
251+
}
252+
}
253+
}
254+
}
255+
Type::Reference(reference) => collect_type_dependencies(&reference.elem, known_names, deps),
256+
Type::Paren(paren) => collect_type_dependencies(&paren.elem, known_names, deps),
257+
Type::Group(group) => collect_type_dependencies(&group.elem, known_names, deps),
258+
Type::Tuple(tuple) => {
259+
for elem in &tuple.elems {
260+
collect_type_dependencies(elem, known_names, deps);
261+
}
262+
}
263+
Type::Array(array) => collect_type_dependencies(&array.elem, known_names, deps),
264+
Type::Slice(slice) => collect_type_dependencies(&slice.elem, known_names, deps),
265+
Type::Ptr(ptr) => collect_type_dependencies(&ptr.elem, known_names, deps),
266+
_ => {}
267+
}
268+
}
269+
270+
/// Converts a nested `use` tree into simple path entries.
271+
fn flatten_use_tree(prefix: Vec<String>, tree: &UseTree, acc: &mut Vec<UseEntry>) {
272+
match tree {
273+
UseTree::Name(name) => {
274+
let mut path = prefix;
275+
path.push(name.ident.to_string());
276+
acc.push(UseEntry { path, rename: None });
277+
}
278+
UseTree::Rename(rename) => {
279+
let mut path = prefix;
280+
path.push(rename.ident.to_string());
281+
acc.push(UseEntry {
282+
path,
283+
rename: Some(rename.rename.to_string()),
284+
});
285+
}
286+
UseTree::Path(path) => {
287+
let mut new_prefix = prefix;
288+
new_prefix.push(path.ident.to_string());
289+
flatten_use_tree(new_prefix, &path.tree, acc);
290+
}
291+
UseTree::Group(group) => {
292+
for item in &group.items {
293+
flatten_use_tree(prefix.clone(), item, acc);
294+
}
295+
}
296+
UseTree::Glob(_) => {}
297+
}
298+
}
299+
300+
/// Takes a `use` statement entry and figures out which version/module defines the type.
301+
fn interpret_flat_use(target_version: &str, entry: &UseEntry) -> Option<ExportInfo> {
302+
if entry.path.is_empty() {
303+
return None;
304+
}
305+
let source_ident = entry.path.last()?.clone();
306+
let exported_ident = entry
307+
.rename
308+
.clone()
309+
.unwrap_or_else(|| source_ident.clone());
310+
311+
match entry.path.first()?.as_str() {
312+
"self" => Some(ExportInfo {
313+
source_version: target_version.to_string(),
314+
source_ident,
315+
exported_ident,
316+
}),
317+
"crate" => {
318+
if entry.path.len() < 3 {
319+
return None;
320+
}
321+
let source_module = &entry.path[1];
322+
if is_version_dir_name(source_module) {
323+
Some(ExportInfo {
324+
source_version: source_module.clone(),
325+
source_ident,
326+
exported_ident,
327+
})
328+
} else {
329+
None
330+
}
331+
}
332+
_ => None,
333+
}
334+
}
335+
336+
/// Returns true if the type Visibility is public.
337+
fn is_public(vis: &Visibility) -> bool { matches!(vis, Visibility::Public(_)) }
338+
339+
/// Checks whether the directory is the root for the version, i.e. the name fits the `vXX` pattern.
340+
fn is_version_dir_name(name: &str) -> bool {
341+
name.starts_with('v') && name.chars().skip(1).all(|c| c.is_ascii_digit())
342+
}

0 commit comments

Comments
 (0)