grimp 3.9__tar.gz → 3.10__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. {grimp-3.9 → grimp-3.10}/PKG-INFO +2 -2
  2. {grimp-3.9 → grimp-3.10}/README.rst +1 -1
  3. {grimp-3.9 → grimp-3.10}/pyproject.toml +1 -1
  4. {grimp-3.9 → grimp-3.10}/rust/Cargo.lock +32 -0
  5. {grimp-3.9 → grimp-3.10}/rust/Cargo.toml +4 -0
  6. {grimp-3.9 → grimp-3.10}/rust/src/errors.rs +1 -0
  7. grimp-3.10/rust/src/filesystem.rs +357 -0
  8. {grimp-3.9 → grimp-3.10}/rust/src/graph/hierarchy_queries.rs +4 -4
  9. {grimp-3.9 → grimp-3.10}/rust/src/graph/higher_order_queries.rs +134 -30
  10. {grimp-3.9 → grimp-3.10}/rust/src/graph/import_chain_queries.rs +47 -16
  11. {grimp-3.9 → grimp-3.10}/rust/src/import_parsing.rs +10 -11
  12. grimp-3.10/rust/src/import_scanning.rs +353 -0
  13. {grimp-3.9 → grimp-3.10}/rust/src/lib.rs +91 -20
  14. grimp-3.10/rust/src/module_finding.rs +60 -0
  15. {grimp-3.9 → grimp-3.10}/rust/tests/large.rs +1 -0
  16. {grimp-3.9 → grimp-3.10}/src/grimp/__init__.py +1 -1
  17. {grimp-3.9 → grimp-3.10}/src/grimp/adaptors/filesystem.py +5 -1
  18. {grimp-3.9 → grimp-3.10}/src/grimp/adaptors/graph.py +7 -3
  19. {grimp-3.9 → grimp-3.10}/src/grimp/application/ports/filesystem.py +34 -0
  20. {grimp-3.9 → grimp-3.10}/src/grimp/application/ports/graph.py +5 -0
  21. grimp-3.10/src/grimp/application/scanning.py +103 -0
  22. {grimp-3.9 → grimp-3.10}/src/grimp/application/usecases.py +8 -105
  23. grimp-3.10/src/grimp/domain/valueobjects.py +83 -0
  24. {grimp-3.9 → grimp-3.10}/src/grimp/main.py +0 -2
  25. grimp-3.9/src/grimp/adaptors/importscanner.py +0 -231
  26. grimp-3.9/src/grimp/algorithms/shortest_path.py +0 -143
  27. grimp-3.9/src/grimp/application/ports/importscanner.py +0 -46
  28. grimp-3.9/src/grimp/domain/__init__.py +0 -0
  29. grimp-3.9/src/grimp/domain/valueobjects.py +0 -106
  30. grimp-3.9/src/grimp/helpers.py +0 -6
  31. {grimp-3.9 → grimp-3.10}/rust/.cargo/config.toml +0 -0
  32. {grimp-3.9 → grimp-3.10}/rust/src/exceptions.rs +0 -0
  33. {grimp-3.9 → grimp-3.10}/rust/src/graph/direct_import_queries.rs +0 -0
  34. {grimp-3.9 → grimp-3.10}/rust/src/graph/graph_manipulation.rs +0 -0
  35. {grimp-3.9 → grimp-3.10}/rust/src/graph/mod.rs +0 -0
  36. {grimp-3.9 → grimp-3.10}/rust/src/graph/pathfinding.rs +0 -0
  37. {grimp-3.9 → grimp-3.10}/rust/src/module_expressions.rs +0 -0
  38. {grimp-3.9 → grimp-3.10}/rust/tests/large_graph.json +0 -0
  39. {grimp-3.9 → grimp-3.10}/src/grimp/adaptors/__init__.py +0 -0
  40. {grimp-3.9 → grimp-3.10}/src/grimp/adaptors/caching.py +0 -0
  41. {grimp-3.9 → grimp-3.10}/src/grimp/adaptors/modulefinder.py +0 -0
  42. {grimp-3.9 → grimp-3.10}/src/grimp/adaptors/packagefinder.py +0 -0
  43. {grimp-3.9 → grimp-3.10}/src/grimp/adaptors/timing.py +0 -0
  44. {grimp-3.9/src/grimp/algorithms → grimp-3.10/src/grimp/application}/__init__.py +0 -0
  45. {grimp-3.9 → grimp-3.10}/src/grimp/application/config.py +0 -0
  46. {grimp-3.9/src/grimp/application → grimp-3.10/src/grimp/application/ports}/__init__.py +0 -0
  47. {grimp-3.9 → grimp-3.10}/src/grimp/application/ports/caching.py +0 -0
  48. {grimp-3.9 → grimp-3.10}/src/grimp/application/ports/modulefinder.py +0 -0
  49. {grimp-3.9 → grimp-3.10}/src/grimp/application/ports/packagefinder.py +0 -0
  50. {grimp-3.9 → grimp-3.10}/src/grimp/application/ports/timing.py +0 -0
  51. {grimp-3.9/src/grimp/application/ports → grimp-3.10/src/grimp/domain}/__init__.py +0 -0
  52. {grimp-3.9 → grimp-3.10}/src/grimp/domain/analysis.py +0 -0
  53. {grimp-3.9 → grimp-3.10}/src/grimp/exceptions.py +0 -0
  54. {grimp-3.9 → grimp-3.10}/src/grimp/py.typed +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: grimp
3
- Version: 3.9
3
+ Version: 3.10
4
4
  Classifier: Development Status :: 5 - Production/Stable
5
5
  Classifier: Intended Audience :: Developers
6
6
  Classifier: License :: OSI Approved :: BSD License
@@ -39,7 +39,7 @@ Grimp
39
39
  :alt: Python versions
40
40
  :target: https://pypi.org/project/grimp/
41
41
 
42
- .. image:: https://github.com/seddonym/grimp/workflows/CI/badge.svg?branch=master
42
+ .. image:: https://github.com/seddonym/grimp/workflows/CI/badge.svg?branch=main
43
43
  :target: https://github.com/seddonym/grimp/actions?workflow=CI
44
44
  :alt: CI Status
45
45
 
@@ -9,7 +9,7 @@ Grimp
9
9
  :alt: Python versions
10
10
  :target: https://pypi.org/project/grimp/
11
11
 
12
- .. image:: https://github.com/seddonym/grimp/workflows/CI/badge.svg?branch=master
12
+ .. image:: https://github.com/seddonym/grimp/workflows/CI/badge.svg?branch=main
13
13
  :target: https://github.com/seddonym/grimp/actions?workflow=CI
14
14
  :alt: CI Status
15
15
 
@@ -8,7 +8,7 @@ module-name = "grimp._rustgrimp"
8
8
 
9
9
  [project]
10
10
  name = "grimp"
11
- version = "3.9"
11
+ version = "3.10"
12
12
  license = {text = "BSD 2-Clause License"}
13
13
  description = "Builds a queryable graph of the imports within one or more Python packages."
14
14
  authors = [
@@ -9,6 +9,7 @@ dependencies = [
9
9
  "bimap",
10
10
  "const_format",
11
11
  "derive-new",
12
+ "encoding_rs",
12
13
  "getset",
13
14
  "indexmap 2.9.0",
14
15
  "itertools 0.14.0",
@@ -21,11 +22,14 @@ dependencies = [
21
22
  "ruff_python_parser",
22
23
  "ruff_source_file",
23
24
  "rustc-hash 2.1.1",
25
+ "serde",
24
26
  "serde_json",
27
+ "serde_yaml",
25
28
  "slotmap",
26
29
  "string-interner",
27
30
  "tap",
28
31
  "thiserror",
32
+ "unindent",
29
33
  ]
30
34
 
31
35
  [[package]]
@@ -134,6 +138,15 @@ version = "1.15.0"
134
138
  source = "registry+https://github.com/rust-lang/crates.io-index"
135
139
  checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
136
140
 
141
+ [[package]]
142
+ name = "encoding_rs"
143
+ version = "0.8.35"
144
+ source = "registry+https://github.com/rust-lang/crates.io-index"
145
+ checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3"
146
+ dependencies = [
147
+ "cfg-if",
148
+ ]
149
+
137
150
  [[package]]
138
151
  name = "equivalent"
139
152
  version = "1.0.2"
@@ -666,6 +679,19 @@ dependencies = [
666
679
  "serde",
667
680
  ]
668
681
 
682
+ [[package]]
683
+ name = "serde_yaml"
684
+ version = "0.9.34+deprecated"
685
+ source = "registry+https://github.com/rust-lang/crates.io-index"
686
+ checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47"
687
+ dependencies = [
688
+ "indexmap 2.9.0",
689
+ "itoa",
690
+ "ryu",
691
+ "serde",
692
+ "unsafe-libyaml",
693
+ ]
694
+
669
695
  [[package]]
670
696
  name = "siphasher"
671
697
  version = "1.0.1"
@@ -810,6 +836,12 @@ version = "0.2.4"
810
836
  source = "registry+https://github.com/rust-lang/crates.io-index"
811
837
  checksum = "7264e107f553ccae879d21fbea1d6724ac785e8c3bfc762137959b5802826ef3"
812
838
 
839
+ [[package]]
840
+ name = "unsafe-libyaml"
841
+ version = "0.2.11"
842
+ source = "registry+https://github.com/rust-lang/crates.io-index"
843
+ checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861"
844
+
813
845
  [[package]]
814
846
  name = "version_check"
815
847
  version = "0.9.5"
@@ -25,6 +25,10 @@ const_format = "0.2.34"
25
25
  ruff_python_parser = { git = "https://github.com/astral-sh/ruff.git", tag = "v0.4.10" }
26
26
  ruff_python_ast = { git = "https://github.com/astral-sh/ruff.git", tag = "v0.4.10" }
27
27
  ruff_source_file = { git = "https://github.com/astral-sh/ruff.git", tag = "v0.4.10" }
28
+ serde = { version = "1.0", features = ["derive"] }
29
+ serde_yaml = "0.9"
30
+ unindent = "0.2.4"
31
+ encoding_rs = "0.8.35"
28
32
 
29
33
  [dependencies.pyo3]
30
34
  version = "0.24.1"
@@ -20,6 +20,7 @@ pub enum GrimpError {
20
20
 
21
21
  #[error("Error parsing python code (line {line_number}, text {text}).")]
22
22
  ParseError {
23
+ module_filename: String,
23
24
  line_number: usize,
24
25
  text: String,
25
26
  #[source]
@@ -0,0 +1,357 @@
1
+ use pyo3::exceptions::{PyFileNotFoundError, PyUnicodeDecodeError};
2
+ use pyo3::prelude::*;
3
+ use regex::Regex;
4
+ use std::collections::HashMap;
5
+ use std::ffi::OsStr;
6
+ use std::fs;
7
+ use std::path::{Path, PathBuf};
8
+ use unindent::unindent;
9
+
10
+ pub trait FileSystem: Send + Sync {
11
+ fn sep(&self) -> String;
12
+
13
+ fn join(&self, components: Vec<String>) -> String;
14
+
15
+ fn split(&self, file_name: &str) -> (String, String);
16
+
17
+ fn exists(&self, file_name: &str) -> bool;
18
+
19
+ fn read(&self, file_name: &str) -> PyResult<String>;
20
+ }
21
+
22
+ #[derive(Clone)]
23
+ #[pyclass]
24
+ pub struct RealBasicFileSystem {}
25
+
26
+ // Implements a BasicFileSystem (defined in grimp.application.ports.filesystem.BasicFileSystem)
27
+ // that actually reads files.
28
+ #[pyclass(name = "RealBasicFileSystem")]
29
+ pub struct PyRealBasicFileSystem {
30
+ pub inner: RealBasicFileSystem,
31
+ }
32
+
33
+ impl FileSystem for RealBasicFileSystem {
34
+ fn sep(&self) -> String {
35
+ std::path::MAIN_SEPARATOR.to_string()
36
+ }
37
+
38
+ fn join(&self, components: Vec<String>) -> String {
39
+ let mut path = PathBuf::new();
40
+ for component in components {
41
+ path.push(component);
42
+ }
43
+ path.to_str().unwrap().to_string()
44
+ }
45
+
46
+ fn split(&self, file_name: &str) -> (String, String) {
47
+ let path = Path::new(file_name);
48
+
49
+ // Get the "tail" part (the file name or last directory)
50
+ let tail = match path.file_name() {
51
+ Some(name) => PathBuf::from(name),
52
+ None => PathBuf::new(), // If there's no file name (e.g., path is a root), return empty
53
+ };
54
+
55
+ // Get the "head" part (the parent directory)
56
+ let head = match path.parent() {
57
+ Some(parent_path) => parent_path.to_path_buf(),
58
+ None => PathBuf::new(), // If there's no parent (e.g., just a filename), return empty
59
+ };
60
+
61
+ (
62
+ head.to_str().unwrap().to_string(),
63
+ tail.to_str().unwrap().to_string(),
64
+ )
65
+ }
66
+
67
+ fn exists(&self, file_name: &str) -> bool {
68
+ Path::new(file_name).is_file()
69
+ }
70
+
71
+ fn read(&self, file_name: &str) -> PyResult<String> {
72
+ // Python files are assumed UTF-8 by default (PEP 686), but they can specify an alternative
73
+ // encoding, which we need to take into account here.
74
+ // See https://peps.python.org/pep-0263/
75
+
76
+ // This method was authored primarily by an LLM.
77
+
78
+ let path = Path::new(file_name);
79
+ let bytes = fs::read(path).map_err(|e| {
80
+ PyFileNotFoundError::new_err(format!("Failed to read file {file_name}: {e}"))
81
+ })?;
82
+
83
+ let s = String::from_utf8_lossy(&bytes);
84
+ let encoding_re = Regex::new(r"^[ \t\f]*#.*?coding[:=][ \t]*([-_.a-zA-Z0-9]+)").unwrap();
85
+
86
+ let mut detected_encoding: Option<String> = None;
87
+
88
+ // Coding specification needs to be in the first two lines, or it's ignored.
89
+ for line in s.lines().take(2) {
90
+ if let Some(captures) = encoding_re.captures(line)
91
+ && let Some(encoding_name) = captures.get(1) {
92
+ detected_encoding = Some(encoding_name.as_str().to_string());
93
+ break;
94
+ }
95
+ }
96
+
97
+ if let Some(enc_name) = detected_encoding {
98
+ let encoding =
99
+ encoding_rs::Encoding::for_label(enc_name.as_bytes()).ok_or_else(|| {
100
+ PyUnicodeDecodeError::new_err(format!(
101
+ "Failed to decode file {file_name} (unknown encoding '{enc_name}')"
102
+ ))
103
+ })?;
104
+ let (decoded_s, _, had_errors) = encoding.decode(&bytes);
105
+ if had_errors {
106
+ Err(PyUnicodeDecodeError::new_err(format!(
107
+ "Failed to decode file {file_name} with encoding '{enc_name}'"
108
+ )))
109
+ } else {
110
+ Ok(decoded_s.into_owned())
111
+ }
112
+ } else {
113
+ // Default to UTF-8 if no encoding is specified
114
+ String::from_utf8(bytes).map_err(|e| {
115
+ PyUnicodeDecodeError::new_err(format!(
116
+ "Failed to decode file {file_name} as UTF-8: {e}"
117
+ ))
118
+ })
119
+ }
120
+ }
121
+ }
122
+
123
+ #[pymethods]
124
+ impl PyRealBasicFileSystem {
125
+ #[new]
126
+ fn new() -> Self {
127
+ PyRealBasicFileSystem {
128
+ inner: RealBasicFileSystem {},
129
+ }
130
+ }
131
+
132
+ #[getter]
133
+ fn sep(&self) -> String {
134
+ self.inner.sep()
135
+ }
136
+
137
+ #[pyo3(signature = (*components))]
138
+ fn join(&self, components: Vec<String>) -> String {
139
+ self.inner.join(components)
140
+ }
141
+
142
+ fn split(&self, file_name: &str) -> (String, String) {
143
+ self.inner.split(file_name)
144
+ }
145
+
146
+ fn exists(&self, file_name: &str) -> bool {
147
+ self.inner.exists(file_name)
148
+ }
149
+
150
+ fn read(&self, file_name: &str) -> PyResult<String> {
151
+ self.inner.read(file_name)
152
+ }
153
+ }
154
+
155
+ type FileSystemContents = HashMap<String, String>;
156
+
157
+ #[derive(Clone)]
158
+ pub struct FakeBasicFileSystem {
159
+ contents: Box<FileSystemContents>,
160
+ }
161
+
162
+ // Implements BasicFileSystem (defined in grimp.application.ports.filesystem.BasicFileSystem).
163
+ #[pyclass(name = "FakeBasicFileSystem")]
164
+ pub struct PyFakeBasicFileSystem {
165
+ pub inner: FakeBasicFileSystem,
166
+ }
167
+
168
+ impl FakeBasicFileSystem {
169
+ fn new(contents: Option<&str>, content_map: Option<HashMap<String, String>>) -> PyResult<Self> {
170
+ let mut parsed_contents = match contents {
171
+ Some(contents) => parse_indented_file_system_string(contents),
172
+ None => HashMap::new(),
173
+ };
174
+ if let Some(content_map) = content_map {
175
+ let unindented_map: HashMap<String, String> = content_map
176
+ .into_iter()
177
+ .map(|(key, val)| (key, unindent(&val).trim().to_string()))
178
+ .collect();
179
+ parsed_contents.extend(unindented_map);
180
+ };
181
+ Ok(FakeBasicFileSystem {
182
+ contents: Box::new(parsed_contents),
183
+ })
184
+ }
185
+ }
186
+
187
+ impl FileSystem for FakeBasicFileSystem {
188
+ fn sep(&self) -> String {
189
+ "/".to_string()
190
+ }
191
+
192
+ fn join(&self, components: Vec<String>) -> String {
193
+ let sep = self.sep();
194
+ components
195
+ .into_iter()
196
+ .map(|c| c.trim_end_matches(&sep).to_string())
197
+ .collect::<Vec<String>>()
198
+ .join(&sep)
199
+ }
200
+
201
+ fn split(&self, file_name: &str) -> (String, String) {
202
+ let path;
203
+ let head;
204
+ let tail;
205
+ if let Some(file_name_without_trailing_slash) = file_name.strip_suffix("/") {
206
+ head = Path::new(file_name_without_trailing_slash);
207
+ tail = OsStr::new("");
208
+ } else {
209
+ path = Path::new(file_name);
210
+ head = path.parent().unwrap_or(Path::new(""));
211
+ tail = path.file_name().unwrap_or(OsStr::new(""));
212
+ }
213
+ (
214
+ head.to_str().unwrap().to_string(),
215
+ tail.to_str().unwrap().to_string(),
216
+ )
217
+ }
218
+
219
+ /// Checks if a file or directory exists within the file system.
220
+ fn exists(&self, file_name: &str) -> bool {
221
+ self.contents.contains_key(file_name)
222
+ }
223
+
224
+ fn read(&self, file_name: &str) -> PyResult<String> {
225
+ match self.contents.get(file_name) {
226
+ Some(file_name) => Ok(file_name.clone()),
227
+ None => Err(PyFileNotFoundError::new_err("")),
228
+ }
229
+ }
230
+ }
231
+
232
+ #[pymethods]
233
+ impl PyFakeBasicFileSystem {
234
+ #[pyo3(signature = (contents=None, content_map=None))]
235
+ #[new]
236
+ fn new(contents: Option<&str>, content_map: Option<HashMap<String, String>>) -> PyResult<Self> {
237
+ Ok(PyFakeBasicFileSystem {
238
+ inner: FakeBasicFileSystem::new(contents, content_map)?,
239
+ })
240
+ }
241
+
242
+ #[getter]
243
+ fn sep(&self) -> String {
244
+ self.inner.sep()
245
+ }
246
+
247
+ #[pyo3(signature = (*components))]
248
+ fn join(&self, components: Vec<String>) -> String {
249
+ self.inner.join(components)
250
+ }
251
+
252
+ fn split(&self, file_name: &str) -> (String, String) {
253
+ self.inner.split(file_name)
254
+ }
255
+
256
+ /// Checks if a file or directory exists within the file system.
257
+ fn exists(&self, file_name: &str) -> bool {
258
+ self.inner.exists(file_name)
259
+ }
260
+
261
+ fn read(&self, file_name: &str) -> PyResult<String> {
262
+ self.inner.read(file_name)
263
+ }
264
+
265
+ // Temporary workaround method for Python tests.
266
+ fn convert_to_basic(&self) -> PyResult<Self> {
267
+ Ok(PyFakeBasicFileSystem {
268
+ inner: self.inner.clone()
269
+ })
270
+ }
271
+ }
272
+
273
+ /// Parses an indented string representing a file system structure
274
+ /// into a HashMap where keys are full file paths.
275
+ /// See tests.adaptors.filesystem.FakeFileSystem for the API.
276
+ pub fn parse_indented_file_system_string(file_system_string: &str) -> HashMap<String, String> {
277
+ let mut file_paths_map: HashMap<String, String> = HashMap::new();
278
+ let mut path_stack: Vec<String> = Vec::new(); // Stores current directory path components
279
+ let mut first_line = true; // Flag to handle the very first path component
280
+
281
+ // Normalize newlines and split into lines
282
+ let buffer = file_system_string.replace("\r\n", "\n");
283
+ let lines: Vec<&str> = buffer.split('\n').collect();
284
+
285
+ for line_raw in lines.clone() {
286
+ let line = line_raw.trim_end(); // Remove trailing whitespace
287
+ if line.is_empty() {
288
+ continue; // Skip empty lines
289
+ }
290
+
291
+ let current_indent = line.chars().take_while(|&c| c.is_whitespace()).count();
292
+ let trimmed_line = line.trim_start();
293
+
294
+ // Assuming 4 spaces per indentation level for calculating depth
295
+ // Adjust this if your indentation standard is different (e.g., 2 spaces, tabs)
296
+ let current_depth = current_indent / 4;
297
+
298
+ if first_line {
299
+ // The first non-empty line sets the base path.
300
+ // It might be absolute (/a/b/) or relative (a/b/).
301
+ let root_component = trimmed_line.trim_end_matches('/').to_string();
302
+ path_stack.push(root_component);
303
+ first_line = false;
304
+ } else {
305
+ // Adjust the path_stack based on indentation level
306
+ // Pop elements from the stack until we reach the correct parent directory depth
307
+ while path_stack.len() > current_depth {
308
+ path_stack.pop();
309
+ }
310
+
311
+ // If the current line is a file, append it to the path for inserting into map,
312
+ // then pop it off so that subsequent siblings are correctly handled.
313
+ // If it's a directory, append it and it stays on the stack for its children.
314
+ let component_to_add = trimmed_line.trim_end_matches('/').to_string();
315
+ if !component_to_add.is_empty() {
316
+ // Avoid pushing empty strings due to lines like just "/"
317
+ path_stack.push(component_to_add);
318
+ }
319
+ }
320
+
321
+ // Construct the full path
322
+ // Join components on the stack. If the first component started with '/',
323
+ // ensure the final path also starts with '/'.
324
+ let full_path = if !path_stack.is_empty() {
325
+ let mut joined = path_stack.join("/");
326
+ // If the original root started with a slash, ensure the final path does too.
327
+ // But be careful not to double-slash if a component is e.g. "/root"
328
+ if lines[0].trim().starts_with('/') && !joined.starts_with('/') {
329
+ joined = format!("/{joined}");
330
+ }
331
+ joined
332
+ } else {
333
+ "".to_string()
334
+ };
335
+
336
+ // If it's a file (doesn't end with '/'), add it to the map
337
+ // A file is not a directory, so its name should be removed from the stack after processing
338
+ // so that sibling items are at the correct level.
339
+ if !trimmed_line.ends_with('/') {
340
+ file_paths_map.insert(full_path, String::new()); // Value can be empty or actual content
341
+ if !path_stack.is_empty() {
342
+ path_stack.pop(); // Pop the file name off the stack
343
+ }
344
+ }
345
+ }
346
+
347
+ // Edge case: If the very first line was a file and it ended up on the stack, it needs to be processed.
348
+ // This handles single-file inputs like "myfile.txt"
349
+ if !path_stack.is_empty()
350
+ && !path_stack.last().unwrap().ends_with('/')
351
+ && !file_paths_map.contains_key(&path_stack.join("/"))
352
+ {
353
+ file_paths_map.insert(path_stack.join("/"), String::new());
354
+ }
355
+
356
+ file_paths_map
357
+ }
@@ -17,7 +17,7 @@ impl Graph {
17
17
  }
18
18
 
19
19
  // TODO(peter) Guarantee order?
20
- pub fn all_modules(&self) -> impl ModuleIterator {
20
+ pub fn all_modules(&self) -> impl ModuleIterator<'_> {
21
21
  self.modules.values()
22
22
  }
23
23
 
@@ -28,7 +28,7 @@ impl Graph {
28
28
  }
29
29
  }
30
30
 
31
- pub fn get_module_children(&self, module: ModuleToken) -> impl ModuleIterator {
31
+ pub fn get_module_children(&self, module: ModuleToken) -> impl ModuleIterator<'_> {
32
32
  let children = match self.module_children.get(module) {
33
33
  Some(children) => children
34
34
  .iter()
@@ -42,7 +42,7 @@ impl Graph {
42
42
  /// Returns an iterator over the passed modules descendants.
43
43
  ///
44
44
  /// Parent modules will be yielded before their child modules.
45
- pub fn get_module_descendants(&self, module: ModuleToken) -> impl ModuleIterator {
45
+ pub fn get_module_descendants(&self, module: ModuleToken) -> impl ModuleIterator<'_> {
46
46
  let mut descendants = self.get_module_children(module).collect::<Vec<_>>();
47
47
  for child in descendants.clone() {
48
48
  descendants.extend(self.get_module_descendants(child.token).collect::<Vec<_>>())
@@ -53,7 +53,7 @@ impl Graph {
53
53
  pub fn find_matching_modules(
54
54
  &self,
55
55
  expression: &ModuleExpression,
56
- ) -> impl ModuleIterator + use<'_> {
56
+ ) -> impl ModuleIterator<'_> + use<'_> {
57
57
  let interner = MODULE_NAMES.read().unwrap();
58
58
  let modules: FxHashSet<_> = self
59
59
  .modules