Skip to content

Commit f98e51b

Browse files
committed
Improve inference of the compile_data attribute
1 parent d7ae3b9 commit f98e51b

4 files changed

Lines changed: 108 additions & 12 deletions

File tree

proto/messages.proto

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,9 @@ message RustImportsResponse {
3131
repeated string imports = 2;
3232
repeated string test_imports = 3;
3333
repeated string extern_mods = 4;
34-
bool success = 5;
35-
string error_msg = 6;
34+
repeated string compile_data = 5;
35+
bool success = 6;
36+
string error_msg = 7;
3637
}
3738

3839
message LockfileCratesRequest {

rust_language/generate.go

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -431,10 +431,14 @@ func (l *rustLang) generateCargoRule(c *config.Config, args *language.GenerateAr
431431
}
432432

433433
srcs := []string{}
434+
compile_data := map[string]struct{}{"Cargo.toml": true}
434435
responses := []*pb.RustImportsResponse{}
435436

436437
for src, response := range importsResponses {
437438
srcs = append(srcs, src)
439+
for _, f := range response.CompileData {
440+
compile_data[f] = true
441+
}
438442
if response != nil {
439443
responses = append(responses, response)
440444
}
@@ -446,7 +450,7 @@ func (l *rustLang) generateCargoRule(c *config.Config, args *language.GenerateAr
446450
newRule.SetAttr("srcs", srcs)
447451
}
448452
newRule.SetAttr("visibility", []string{"//visibility:public"})
449-
newRule.SetAttr("compile_data", []string{"Cargo.toml"})
453+
newRule.SetAttr("compile_data", setToVector(compile_data))
450454

451455
if targetName != crateName {
452456
newRule.SetAttr("crate_name", crateName)
@@ -494,10 +498,14 @@ func (l *rustLang) generateBuildScript(c *config.Config, args *language.Generate
494498
l.discoverModule(c, "build.rs", enabledFeatures, args, &importsResponses, true)
495499

496500
srcs := []string{}
501+
compile_data := map[string]struct{}{"Cargo.toml": true}
497502
responses := []*pb.RustImportsResponse{}
498503

499504
for src, response := range importsResponses {
500505
srcs = append(srcs, src)
506+
for _, f := range response.CompileData {
507+
compile_data[f] = true
508+
}
501509
if response != nil {
502510
responses = append(responses, response)
503511
}
@@ -506,7 +514,7 @@ func (l *rustLang) generateBuildScript(c *config.Config, args *language.Generate
506514
newRule := rule.NewRule("cargo_build_script", "build_script")
507515
newRule.SetAttr("srcs", srcs)
508516
newRule.SetAttr("visibility", []string{"//visibility:public"})
509-
newRule.SetAttr("compile_data", []string{"Cargo.toml"})
517+
newRule.SetAttr("compile_data", setToVector(compile_data))
510518
newRule.SetAttr("crate_root", "build.rs")
511519

512520
cfg := l.GetConfig(args.Config)
@@ -589,3 +597,11 @@ func fileExists(path string, args *language.GenerateArgs) bool {
589597
_, err := os.Stat(fullPath)
590598
return err == nil
591599
}
600+
601+
func setToVector(src map[string]bool) []string {
602+
result := []string{}
603+
for key := range src {
604+
result = append(result, key)
605+
}
606+
return result
607+
}

rust_parser/main.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ fn handle_rust_imports_request(
3939
response.imports = rust_imports.imports;
4040
response.test_imports = rust_imports.test_imports;
4141
response.extern_mods = rust_imports.extern_mods;
42+
response.compile_data = rust_imports.compile_data;
4243
}
4344
Err(err) => {
4445
// Don't crash gazelle if we encounter an error, instead bubble it up so that we can

rust_parser/parser.rs

Lines changed: 86 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use std::collections::{HashSet, VecDeque};
44
use std::error::Error;
55
use std::fs::File;
66
use std::io::Read;
7-
use std::path::PathBuf;
7+
use std::path::{Path, PathBuf};
88

99
use syn::ext::IdentExt;
1010
use syn::parse_file;
@@ -16,6 +16,7 @@ pub struct RustImports {
1616
pub imports: Vec<String>,
1717
pub test_imports: Vec<String>,
1818
pub extern_mods: Vec<String>,
19+
pub compile_data: Vec<String>,
1920
}
2021

2122
#[derive(Debug, Default)]
@@ -44,15 +45,16 @@ pub fn parse_imports(
4445
let mut contents = String::new();
4546
file.read_to_string(&mut contents)?;
4647

47-
parse_imports_from_str(&contents, enabled_features)
48+
parse_imports_from_str(&contents, enabled_features, path)
4849
}
4950

5051
pub fn parse_imports_from_str(
5152
contents: &str,
5253
enabled_features: &[String],
54+
path: PathBuf,
5355
) -> Result<RustImports, Box<dyn Error>> {
5456
let ast = parse_file(contents)?;
55-
let mut visitor = AstVisitor::new(enabled_features);
57+
let mut visitor = AstVisitor::new(enabled_features, path);
5658
visitor.visit_file(&ast);
5759

5860
let mut root_scope = visitor.mod_stack.pop_back().expect("no root scope");
@@ -70,7 +72,8 @@ pub fn parse_imports_from_str(
7072
hints: visitor.hints,
7173
imports: filter_imports(root_scope.imports),
7274
test_imports: filter_imports(root_scope.test_imports),
73-
extern_mods: visitor.extern_mods,
75+
extern_mods: visitor.extern_mods.into_iter().collect(),
76+
compile_data: visitor.compile_data.into_iter().collect(),
7477
})
7578
}
7679

@@ -160,33 +163,39 @@ impl Scope<'_> {
160163

161164
#[derive(Debug)]
162165
struct AstVisitor<'ast> {
166+
/// The path of the file we are visiting.
167+
this_file: PathBuf,
163168
/// stack of mods in scope
164169
mod_stack: VecDeque<Scope<'ast>>,
165170
/// all mods that are currently in scope (including parent scopes)
166171
scope_mods: HashSet<Ident<'ast>>,
167172
/// collected hints
168173
hints: Hints,
169174
/// bare mods defined in external files
170-
extern_mods: Vec<String>,
175+
extern_mods: HashSet<String>,
171176
/// mods that are disallowed from being added to the current scope; this is currently only used
172177
/// for a hack, see below
173178
mod_denylist: HashSet<Ident<'ast>>,
174179
/// Enabled features
175180
enabled_features: HashSet<String>,
181+
/// Files that are included via include_str! and include_bytes! macros.
182+
compile_data: HashSet<String>,
176183
}
177184

178185
impl AstVisitor<'_> {
179-
fn new(enabled_features: &[String]) -> Self {
186+
fn new(enabled_features: &[String], path: PathBuf) -> Self {
180187
let mut mod_stack = VecDeque::new();
181188
mod_stack.push_back(Scope::default());
182189

183190
Self {
191+
this_file: path,
184192
mod_stack,
185193
scope_mods: HashSet::default(),
186194
hints: Hints::default(),
187-
extern_mods: Vec::default(),
195+
extern_mods: HashSet::new(),
188196
mod_denylist: HashSet::new(),
189197
enabled_features: enabled_features.iter().cloned().collect(),
198+
compile_data: HashSet::new(),
190199
}
191200
}
192201
}
@@ -535,7 +544,7 @@ impl<'ast> Visit<'ast> for AstVisitor<'ast> {
535544

536545
if self.is_root_scope() && node.content.is_none() {
537546
// this mod is defined in a different file
538-
self.extern_mods.push(node.ident.unraw().to_string());
547+
self.extern_mods.insert(node.ident.unraw().to_string());
539548
}
540549

541550
self.add_mod(&node.ident);
@@ -605,6 +614,48 @@ impl<'ast> Visit<'ast> for AstVisitor<'ast> {
605614
}
606615
visit::visit_item_macro(self, node);
607616
}
617+
618+
fn visit_expr_macro(&mut self, node: &'ast syn::ExprMacro) {
619+
let macro_ident = node.mac.path.get_ident();
620+
621+
if let Some(ident) = macro_ident {
622+
if ident == "include_str" || ident == "include_bytes" {
623+
// Respect ignored scopes
624+
if self.is_ignored_scope() {
625+
return;
626+
}
627+
628+
// Parse macro input: expect a single string literal
629+
if let Ok(syn::Expr::Lit(syn::ExprLit {
630+
lit: syn::Lit::Str(lit),
631+
..
632+
})) = syn::parse2::<syn::Expr>(node.mac.tokens.clone())
633+
{
634+
let included_path = PathBuf::from(lit.value());
635+
636+
if included_path.is_absolute() {
637+
panic!(
638+
"included paths must not be absolute: {}",
639+
included_path.display()
640+
)
641+
} else {
642+
let dir = self
643+
.this_file
644+
.parent()
645+
.unwrap_or_else(|| Path::new(""))
646+
.to_path_buf();
647+
let combined = dir.join(included_path);
648+
match normalize_path(&combined).to_str() {
649+
None => panic!("Invalid unicode in the path: {}", combined.display()),
650+
Some(x) => self.compile_data.insert(x.to_string()),
651+
};
652+
}
653+
}
654+
}
655+
}
656+
657+
visit::visit_expr_macro(self, node);
658+
}
608659
}
609660

610661
fn parse_use_imports<'ast>(use_tree: &'ast syn::UseTree, imports: &mut HashSet<Ident<'ast>>) {
@@ -623,3 +674,30 @@ fn parse_use_imports<'ast>(use_tree: &'ast syn::UseTree, imports: &mut HashSet<I
623674
_ => (),
624675
}
625676
}
677+
678+
/// Normalize a path by resolving `.` and `..` without touching the filesystem
679+
fn normalize_path(path: &Path) -> PathBuf {
680+
// let components = path.components().peekable();
681+
let mut stack = Vec::new();
682+
683+
for component in path.components() {
684+
match component {
685+
std::path::Component::ParentDir => {
686+
if let Some(last) = stack.last() {
687+
// Only pop if last is a normal component, not RootDir
688+
if matches!(last, std::path::Component::Normal(_)) {
689+
stack.pop();
690+
} else {
691+
stack.push(component);
692+
}
693+
} else {
694+
stack.push(component);
695+
}
696+
}
697+
std::path::Component::CurDir => { /* skip `.` */ }
698+
_ => stack.push(component),
699+
}
700+
}
701+
702+
stack.iter().collect()
703+
}

0 commit comments

Comments
 (0)