grimp 3.6__tar.gz → 3.7__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 (50) hide show
  1. {grimp-3.6 → grimp-3.7}/PKG-INFO +1 -1
  2. {grimp-3.6 → grimp-3.7}/pyproject.toml +1 -1
  3. {grimp-3.6 → grimp-3.7}/rust/Cargo.lock +107 -3
  4. {grimp-3.6 → grimp-3.7}/rust/Cargo.toml +3 -0
  5. {grimp-3.6 → grimp-3.7}/rust/src/errors.rs +7 -1
  6. {grimp-3.6 → grimp-3.7}/rust/src/exceptions.rs +5 -0
  7. grimp-3.7/rust/src/graph/direct_import_queries.rs +126 -0
  8. {grimp-3.6 → grimp-3.7}/rust/src/graph/graph_manipulation.rs +13 -7
  9. grimp-3.7/rust/src/graph/hierarchy_queries.rs +105 -0
  10. {grimp-3.6 → grimp-3.7}/rust/src/graph/import_chain_queries.rs +2 -2
  11. {grimp-3.6 → grimp-3.7}/rust/src/graph/mod.rs +22 -11
  12. {grimp-3.6 → grimp-3.7}/rust/src/graph/pathfinding.rs +2 -1
  13. {grimp-3.6 → grimp-3.7}/rust/src/lib.rs +81 -36
  14. grimp-3.7/rust/src/module_expressions.rs +196 -0
  15. {grimp-3.6 → grimp-3.7}/src/grimp/__init__.py +3 -2
  16. {grimp-3.6 → grimp-3.7}/src/grimp/adaptors/graph.py +32 -4
  17. {grimp-3.6 → grimp-3.7}/src/grimp/application/ports/graph.py +77 -2
  18. {grimp-3.6 → grimp-3.7}/src/grimp/exceptions.py +8 -0
  19. grimp-3.6/rust/src/graph/direct_import_queries.rs +0 -57
  20. grimp-3.6/rust/src/graph/hierarchy_queries.rs +0 -50
  21. {grimp-3.6 → grimp-3.7}/README.rst +0 -0
  22. {grimp-3.6 → grimp-3.7}/rust/.cargo/config.toml +0 -0
  23. {grimp-3.6 → grimp-3.7}/rust/src/graph/higher_order_queries.rs +0 -0
  24. {grimp-3.6 → grimp-3.7}/rust/tests/large.rs +0 -0
  25. {grimp-3.6 → grimp-3.7}/rust/tests/large_graph.json +0 -0
  26. {grimp-3.6 → grimp-3.7}/src/grimp/adaptors/__init__.py +0 -0
  27. {grimp-3.6 → grimp-3.7}/src/grimp/adaptors/caching.py +0 -0
  28. {grimp-3.6 → grimp-3.7}/src/grimp/adaptors/filesystem.py +0 -0
  29. {grimp-3.6 → grimp-3.7}/src/grimp/adaptors/importscanner.py +0 -0
  30. {grimp-3.6 → grimp-3.7}/src/grimp/adaptors/modulefinder.py +0 -0
  31. {grimp-3.6 → grimp-3.7}/src/grimp/adaptors/packagefinder.py +0 -0
  32. {grimp-3.6 → grimp-3.7}/src/grimp/adaptors/timing.py +0 -0
  33. {grimp-3.6 → grimp-3.7}/src/grimp/algorithms/__init__.py +0 -0
  34. {grimp-3.6 → grimp-3.7}/src/grimp/algorithms/shortest_path.py +0 -0
  35. {grimp-3.6 → grimp-3.7}/src/grimp/application/__init__.py +0 -0
  36. {grimp-3.6 → grimp-3.7}/src/grimp/application/config.py +0 -0
  37. {grimp-3.6 → grimp-3.7}/src/grimp/application/ports/__init__.py +0 -0
  38. {grimp-3.6 → grimp-3.7}/src/grimp/application/ports/caching.py +0 -0
  39. {grimp-3.6 → grimp-3.7}/src/grimp/application/ports/filesystem.py +0 -0
  40. {grimp-3.6 → grimp-3.7}/src/grimp/application/ports/importscanner.py +0 -0
  41. {grimp-3.6 → grimp-3.7}/src/grimp/application/ports/modulefinder.py +0 -0
  42. {grimp-3.6 → grimp-3.7}/src/grimp/application/ports/packagefinder.py +0 -0
  43. {grimp-3.6 → grimp-3.7}/src/grimp/application/ports/timing.py +0 -0
  44. {grimp-3.6 → grimp-3.7}/src/grimp/application/usecases.py +0 -0
  45. {grimp-3.6 → grimp-3.7}/src/grimp/domain/__init__.py +0 -0
  46. {grimp-3.6 → grimp-3.7}/src/grimp/domain/analysis.py +0 -0
  47. {grimp-3.6 → grimp-3.7}/src/grimp/domain/valueobjects.py +0 -0
  48. {grimp-3.6 → grimp-3.7}/src/grimp/helpers.py +0 -0
  49. {grimp-3.6 → grimp-3.7}/src/grimp/main.py +0 -0
  50. {grimp-3.6 → grimp-3.7}/src/grimp/py.typed +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: grimp
3
- Version: 3.6
3
+ Version: 3.7
4
4
  Classifier: Development Status :: 5 - Production/Stable
5
5
  Classifier: Intended Audience :: Developers
6
6
  Classifier: License :: OSI Approved :: BSD License
@@ -8,7 +8,7 @@ module-name = "grimp._rustgrimp"
8
8
 
9
9
  [project]
10
10
  name = "grimp"
11
- version = "3.6"
11
+ version = "3.7"
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 = [
@@ -7,13 +7,16 @@ name = "_rustgrimp"
7
7
  version = "0.1.0"
8
8
  dependencies = [
9
9
  "bimap",
10
+ "const_format",
10
11
  "derive-new",
11
12
  "getset",
12
- "indexmap",
13
+ "indexmap 2.7.1",
13
14
  "itertools",
14
15
  "lazy_static",
16
+ "parameterized",
15
17
  "pyo3",
16
18
  "rayon",
19
+ "regex",
17
20
  "rustc-hash",
18
21
  "serde_json",
19
22
  "slotmap",
@@ -22,6 +25,15 @@ dependencies = [
22
25
  "thiserror",
23
26
  ]
24
27
 
28
+ [[package]]
29
+ name = "aho-corasick"
30
+ version = "1.1.3"
31
+ source = "registry+https://github.com/rust-lang/crates.io-index"
32
+ checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
33
+ dependencies = [
34
+ "memchr",
35
+ ]
36
+
25
37
  [[package]]
26
38
  name = "autocfg"
27
39
  version = "1.4.0"
@@ -40,6 +52,26 @@ version = "1.0.0"
40
52
  source = "registry+https://github.com/rust-lang/crates.io-index"
41
53
  checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
42
54
 
55
+ [[package]]
56
+ name = "const_format"
57
+ version = "0.2.34"
58
+ source = "registry+https://github.com/rust-lang/crates.io-index"
59
+ checksum = "126f97965c8ad46d6d9163268ff28432e8f6a1196a55578867832e3049df63dd"
60
+ dependencies = [
61
+ "const_format_proc_macros",
62
+ ]
63
+
64
+ [[package]]
65
+ name = "const_format_proc_macros"
66
+ version = "0.2.34"
67
+ source = "registry+https://github.com/rust-lang/crates.io-index"
68
+ checksum = "1d57c2eccfb16dbac1f4e61e206105db5820c9d26c3c472bc17c774259ef7744"
69
+ dependencies = [
70
+ "proc-macro2",
71
+ "quote",
72
+ "unicode-xid",
73
+ ]
74
+
43
75
  [[package]]
44
76
  name = "crossbeam-deque"
45
77
  version = "0.8.6"
@@ -106,6 +138,12 @@ dependencies = [
106
138
  "syn",
107
139
  ]
108
140
 
141
+ [[package]]
142
+ name = "hashbrown"
143
+ version = "0.12.3"
144
+ source = "registry+https://github.com/rust-lang/crates.io-index"
145
+ checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
146
+
109
147
  [[package]]
110
148
  name = "hashbrown"
111
149
  version = "0.15.2"
@@ -121,6 +159,16 @@ version = "0.5.0"
121
159
  source = "registry+https://github.com/rust-lang/crates.io-index"
122
160
  checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
123
161
 
162
+ [[package]]
163
+ name = "indexmap"
164
+ version = "1.9.3"
165
+ source = "registry+https://github.com/rust-lang/crates.io-index"
166
+ checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99"
167
+ dependencies = [
168
+ "autocfg",
169
+ "hashbrown 0.12.3",
170
+ ]
171
+
124
172
  [[package]]
125
173
  name = "indexmap"
126
174
  version = "2.7.1"
@@ -128,7 +176,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
128
176
  checksum = "8c9c992b02b5b4c94ea26e32fe5bccb7aa7d9f390ab5c1221ff895bc7ea8b652"
129
177
  dependencies = [
130
178
  "equivalent",
131
- "hashbrown",
179
+ "hashbrown 0.15.2",
132
180
  ]
133
181
 
134
182
  [[package]]
@@ -185,6 +233,27 @@ version = "1.20.2"
185
233
  source = "registry+https://github.com/rust-lang/crates.io-index"
186
234
  checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775"
187
235
 
236
+ [[package]]
237
+ name = "parameterized"
238
+ version = "2.0.0"
239
+ source = "registry+https://github.com/rust-lang/crates.io-index"
240
+ checksum = "194bf497674dda552d4f1bd24d325f828f425876c9d522fcb1810cd527e0bd4e"
241
+ dependencies = [
242
+ "parameterized-macro",
243
+ ]
244
+
245
+ [[package]]
246
+ name = "parameterized-macro"
247
+ version = "2.0.0"
248
+ source = "registry+https://github.com/rust-lang/crates.io-index"
249
+ checksum = "1374bca5edab7a317c4ffbc9df1e239ceb7dcf5426b6b403474408442a9777ac"
250
+ dependencies = [
251
+ "indexmap 1.9.3",
252
+ "proc-macro2",
253
+ "quote",
254
+ "syn",
255
+ ]
256
+
188
257
  [[package]]
189
258
  name = "portable-atomic"
190
259
  version = "1.10.0"
@@ -314,6 +383,35 @@ dependencies = [
314
383
  "crossbeam-utils",
315
384
  ]
316
385
 
386
+ [[package]]
387
+ name = "regex"
388
+ version = "1.11.1"
389
+ source = "registry+https://github.com/rust-lang/crates.io-index"
390
+ checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
391
+ dependencies = [
392
+ "aho-corasick",
393
+ "memchr",
394
+ "regex-automata",
395
+ "regex-syntax",
396
+ ]
397
+
398
+ [[package]]
399
+ name = "regex-automata"
400
+ version = "0.4.9"
401
+ source = "registry+https://github.com/rust-lang/crates.io-index"
402
+ checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908"
403
+ dependencies = [
404
+ "aho-corasick",
405
+ "memchr",
406
+ "regex-syntax",
407
+ ]
408
+
409
+ [[package]]
410
+ name = "regex-syntax"
411
+ version = "0.8.5"
412
+ source = "registry+https://github.com/rust-lang/crates.io-index"
413
+ checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
414
+
317
415
  [[package]]
318
416
  name = "rustc-hash"
319
417
  version = "2.1.0"
@@ -373,7 +471,7 @@ version = "0.18.0"
373
471
  source = "registry+https://github.com/rust-lang/crates.io-index"
374
472
  checksum = "1a3275464d7a9f2d4cac57c89c2ef96a8524dba2864c8d6f82e3980baf136f9b"
375
473
  dependencies = [
376
- "hashbrown",
474
+ "hashbrown 0.15.2",
377
475
  "serde",
378
476
  ]
379
477
 
@@ -426,6 +524,12 @@ version = "1.0.14"
426
524
  source = "registry+https://github.com/rust-lang/crates.io-index"
427
525
  checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83"
428
526
 
527
+ [[package]]
528
+ name = "unicode-xid"
529
+ version = "0.2.6"
530
+ source = "registry+https://github.com/rust-lang/crates.io-index"
531
+ checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
532
+
429
533
  [[package]]
430
534
  name = "unindent"
431
535
  version = "0.2.3"
@@ -20,6 +20,8 @@ itertools = "0.14.0"
20
20
  tap = "1.0.1"
21
21
  rustc-hash = "2.1.0"
22
22
  indexmap = "2.7.1"
23
+ regex = "1.11.1"
24
+ const_format = "0.2.34"
23
25
 
24
26
  [dependencies.pyo3]
25
27
  version = "0.23.4"
@@ -29,4 +31,5 @@ extension-module = ["pyo3/extension-module"]
29
31
  default = ["extension-module"]
30
32
 
31
33
  [dev-dependencies]
34
+ parameterized = "2.0.0"
32
35
  serde_json = "1.0.137"
@@ -1,4 +1,4 @@
1
- use crate::exceptions::{ModuleNotPresent, NoSuchContainer};
1
+ use crate::exceptions::{InvalidModuleExpression, ModuleNotPresent, NoSuchContainer};
2
2
  use pyo3::exceptions::PyValueError;
3
3
  use pyo3::PyErr;
4
4
  use thiserror::Error;
@@ -13,6 +13,9 @@ pub enum GrimpError {
13
13
 
14
14
  #[error("Modules have shared descendants.")]
15
15
  SharedDescendants,
16
+
17
+ #[error("{0} is not a valid module expression.")]
18
+ InvalidModuleExpression(String),
16
19
  }
17
20
 
18
21
  pub type GrimpResult<T> = Result<T, GrimpError>;
@@ -24,6 +27,9 @@ impl From<GrimpError> for PyErr {
24
27
  GrimpError::ModuleNotPresent(_) => ModuleNotPresent::new_err(value.to_string()),
25
28
  GrimpError::NoSuchContainer(_) => NoSuchContainer::new_err(value.to_string()),
26
29
  GrimpError::SharedDescendants => PyValueError::new_err(value.to_string()),
30
+ GrimpError::InvalidModuleExpression(_) => {
31
+ InvalidModuleExpression::new_err(value.to_string())
32
+ }
27
33
  }
28
34
  }
29
35
  }
@@ -2,3 +2,8 @@ use pyo3::create_exception;
2
2
 
3
3
  create_exception!(_rustgrimp, ModuleNotPresent, pyo3::exceptions::PyException);
4
4
  create_exception!(_rustgrimp, NoSuchContainer, pyo3::exceptions::PyException);
5
+ create_exception!(
6
+ _rustgrimp,
7
+ InvalidModuleExpression,
8
+ pyo3::exceptions::PyException
9
+ );
@@ -0,0 +1,126 @@
1
+ use crate::errors::{GrimpError, GrimpResult};
2
+ use crate::graph::{
3
+ ExtendWithDescendants, Graph, ImportDetails, ModuleToken, EMPTY_IMPORT_DETAILS,
4
+ EMPTY_MODULE_TOKENS, MODULE_NAMES,
5
+ };
6
+ use crate::module_expressions::ModuleExpression;
7
+ use rustc_hash::FxHashSet;
8
+
9
+ impl Graph {
10
+ pub fn count_imports(&self) -> usize {
11
+ self.imports.values().map(|imports| imports.len()).sum()
12
+ }
13
+
14
+ pub fn direct_import_exists(
15
+ &self,
16
+ importer: ModuleToken,
17
+ imported: ModuleToken,
18
+ as_packages: bool,
19
+ ) -> GrimpResult<bool> {
20
+ let mut importer: FxHashSet<_> = importer.into();
21
+ let mut imported: FxHashSet<_> = imported.into();
22
+ if as_packages {
23
+ importer.extend_with_descendants(self);
24
+ imported.extend_with_descendants(self);
25
+ if !(&importer & &imported).is_empty() {
26
+ return Err(GrimpError::SharedDescendants);
27
+ }
28
+ }
29
+
30
+ let direct_imports = importer
31
+ .iter()
32
+ .flat_map(|module| self.imports.get(*module).unwrap().iter().cloned())
33
+ .collect::<FxHashSet<ModuleToken>>();
34
+
35
+ Ok(!(&direct_imports & &imported).is_empty())
36
+ }
37
+
38
+ pub fn modules_directly_imported_by(&self, importer: ModuleToken) -> &FxHashSet<ModuleToken> {
39
+ self.imports.get(importer).unwrap_or(&EMPTY_MODULE_TOKENS)
40
+ }
41
+
42
+ pub fn modules_that_directly_import(&self, imported: ModuleToken) -> &FxHashSet<ModuleToken> {
43
+ self.reverse_imports
44
+ .get(imported)
45
+ .unwrap_or(&EMPTY_MODULE_TOKENS)
46
+ }
47
+
48
+ pub fn get_import_details(
49
+ &self,
50
+ importer: ModuleToken,
51
+ imported: ModuleToken,
52
+ ) -> &FxHashSet<ImportDetails> {
53
+ match self.import_details.get(&(importer, imported)) {
54
+ Some(import_details) => import_details,
55
+ None => &EMPTY_IMPORT_DETAILS,
56
+ }
57
+ }
58
+
59
+ pub fn find_matching_direct_imports(
60
+ &self,
61
+ importer_expression: &ModuleExpression,
62
+ imported_expression: &ModuleExpression,
63
+ ) -> FxHashSet<(ModuleToken, ModuleToken)> {
64
+ let interner = MODULE_NAMES.read().unwrap();
65
+ self.imports
66
+ .iter()
67
+ .flat_map(|(importer, imports)| {
68
+ imports
69
+ .iter()
70
+ .cloned()
71
+ .map(move |imported| (importer, imported))
72
+ })
73
+ .filter(|(importer, imported)| {
74
+ let importer = self.get_module(*importer).unwrap();
75
+ let importer_name = interner.resolve(importer.interned_name).unwrap();
76
+ let imported = self.get_module(*imported).unwrap();
77
+ let imported_name = interner.resolve(imported.interned_name).unwrap();
78
+ importer_expression.is_match(importer_name)
79
+ && imported_expression.is_match(imported_name)
80
+ })
81
+ .collect()
82
+ }
83
+ }
84
+
85
+ #[cfg(test)]
86
+ mod test {
87
+ use super::*;
88
+
89
+ #[test]
90
+ fn test_find_matching_direct_imports() {
91
+ let mut graph = Graph::default();
92
+
93
+ let _ = graph.get_or_add_module("pkg.animals").token;
94
+ let dog = graph.get_or_add_module("pkg.animals.dog").token;
95
+ let cat = graph.get_or_add_module("pkg.animals.cat").token;
96
+ let _ = graph.get_or_add_module("pkg.food").token;
97
+ let chicken = graph.get_or_add_module("pkg.food.chicken").token;
98
+ let fish = graph.get_or_add_module("pkg.food.fish").token;
99
+ let _ = graph.get_or_add_module("pkg.colors").token;
100
+ let golden = graph.get_or_add_module("pkg.colors.golden").token;
101
+ let ginger = graph.get_or_add_module("pkg.colors.ginger").token;
102
+ let _ = graph.get_or_add_module("pkg.shops").token;
103
+ let tesco = graph.get_or_add_module("pkg.shops.tesco").token;
104
+ let coop = graph.get_or_add_module("pkg.shops.coop").token;
105
+
106
+ // Should match
107
+ graph.add_import(dog, chicken);
108
+ graph.add_import(cat, fish);
109
+ // Should not match: Imported does not match
110
+ graph.add_import(dog, golden);
111
+ graph.add_import(cat, ginger);
112
+ // Should not match: Importer does not match
113
+ graph.add_import(tesco, chicken);
114
+ graph.add_import(coop, fish);
115
+
116
+ let importer_expression = "pkg.animals.*".parse().unwrap();
117
+ let imported_expression = "pkg.food.*".parse().unwrap();
118
+ let matching_imports =
119
+ graph.find_matching_direct_imports(&importer_expression, &imported_expression);
120
+
121
+ assert_eq!(
122
+ matching_imports,
123
+ FxHashSet::from_iter([(dog, chicken), (cat, fish)])
124
+ );
125
+ }
126
+ }
@@ -1,4 +1,6 @@
1
- use crate::graph::{Graph, ImportDetails, Module, ModuleIterator, ModuleToken, MODULE_NAMES};
1
+ use crate::graph::{
2
+ Graph, ImportDetails, Module, ModuleIterator, ModuleToken, IMPORT_LINE_CONTENTS, MODULE_NAMES,
3
+ };
2
4
  use rustc_hash::FxHashSet;
3
5
  use slotmap::secondary::Entry;
4
6
 
@@ -31,7 +33,7 @@ impl Graph {
31
33
  } else {
32
34
  let module = self.modules.insert_with_key(|token| Module {
33
35
  token,
34
- name,
36
+ interned_name: name,
35
37
  is_invisible: !ancestor_names.is_empty(),
36
38
  is_squashed: false,
37
39
  });
@@ -116,7 +118,7 @@ impl Graph {
116
118
  &mut self,
117
119
  importer: ModuleToken,
118
120
  imported: ModuleToken,
119
- line_number: usize,
121
+ line_number: u32,
120
122
  line_contents: &str,
121
123
  ) {
122
124
  self.imports
@@ -129,10 +131,14 @@ impl Graph {
129
131
  .unwrap()
130
132
  .or_default()
131
133
  .insert(importer);
132
- self.import_details
133
- .entry((importer, imported))
134
- .or_default()
135
- .insert(ImportDetails::new(line_number, line_contents.to_owned()));
134
+ {
135
+ let mut interner = IMPORT_LINE_CONTENTS.write().unwrap();
136
+ let line_contents = interner.get_or_intern(line_contents);
137
+ self.import_details
138
+ .entry((importer, imported))
139
+ .or_default()
140
+ .insert(ImportDetails::new(line_number, line_contents));
141
+ }
136
142
  }
137
143
 
138
144
  pub fn remove_import(&mut self, importer: ModuleToken, imported: ModuleToken) {
@@ -0,0 +1,105 @@
1
+ use crate::graph::{Graph, Module, ModuleIterator, ModuleToken, MODULE_NAMES};
2
+ use crate::module_expressions::ModuleExpression;
3
+ use rustc_hash::FxHashSet;
4
+
5
+ impl Graph {
6
+ pub fn get_module_by_name(&self, name: &str) -> Option<&Module> {
7
+ let interner = MODULE_NAMES.read().unwrap();
8
+ let name = interner.get(name)?;
9
+ match self.modules_by_name.get_by_left(&name) {
10
+ Some(token) => self.get_module(*token),
11
+ None => None,
12
+ }
13
+ }
14
+
15
+ pub fn get_module(&self, module: ModuleToken) -> Option<&Module> {
16
+ self.modules.get(module)
17
+ }
18
+
19
+ // TODO(peter) Guarantee order?
20
+ pub fn all_modules(&self) -> impl ModuleIterator {
21
+ self.modules.values()
22
+ }
23
+
24
+ pub fn get_module_parent(&self, module: ModuleToken) -> Option<&Module> {
25
+ match self.module_parents.get(module) {
26
+ Some(parent) => parent.map(|parent| self.get_module(parent).unwrap()),
27
+ None => None,
28
+ }
29
+ }
30
+
31
+ pub fn get_module_children(&self, module: ModuleToken) -> impl ModuleIterator {
32
+ let children = match self.module_children.get(module) {
33
+ Some(children) => children
34
+ .iter()
35
+ .map(|child| self.get_module(*child).unwrap())
36
+ .collect(),
37
+ None => Vec::new(),
38
+ };
39
+ children.into_iter()
40
+ }
41
+
42
+ /// Returns an iterator over the passed modules descendants.
43
+ ///
44
+ /// Parent modules will be yielded before their child modules.
45
+ pub fn get_module_descendants(&self, module: ModuleToken) -> impl ModuleIterator {
46
+ let mut descendants = self.get_module_children(module).collect::<Vec<_>>();
47
+ for child in descendants.clone() {
48
+ descendants.extend(self.get_module_descendants(child.token).collect::<Vec<_>>())
49
+ }
50
+ descendants.into_iter()
51
+ }
52
+
53
+ pub fn find_matching_modules(&self, expression: &ModuleExpression) -> impl ModuleIterator {
54
+ let interner = MODULE_NAMES.read().unwrap();
55
+ let modules: FxHashSet<_> = self
56
+ .modules
57
+ .values()
58
+ .filter(|m| expression.is_match(interner.resolve(m.interned_name).unwrap()))
59
+ .collect();
60
+ modules.into_iter()
61
+ }
62
+ }
63
+
64
+ #[cfg(test)]
65
+ mod tests {
66
+ use super::*;
67
+ use crate::graph::ModuleIterator;
68
+ use derive_new::new;
69
+ use parameterized::parameterized;
70
+
71
+ #[derive(Debug, new)]
72
+ struct FindMatchingModulesTestCase<'a> {
73
+ expression: &'a str,
74
+ #[new(into)]
75
+ expected_matching_modules: Vec<&'a str>,
76
+ }
77
+
78
+ #[parameterized(case = {
79
+ FindMatchingModulesTestCase::new("foo", ["foo"]),
80
+ FindMatchingModulesTestCase::new("foo.*", ["foo.bar"]),
81
+ FindMatchingModulesTestCase::new("foo.**", ["foo.bar", "foo.bar.baz"]),
82
+ })]
83
+ fn test_find_matching_modules(case: FindMatchingModulesTestCase) {
84
+ // The test here is just a sense check - we don't need to check again here how all the
85
+ // different types of module expressions work since that is already tested
86
+ // thoroughly elsewhere.
87
+ let mut graph = Graph::default();
88
+ graph.get_or_add_module("foo");
89
+ graph.get_or_add_module("foo.bar");
90
+ graph.get_or_add_module("foo.bar.baz");
91
+
92
+ let expression = case.expression.parse().unwrap();
93
+
94
+ let matching_modules: FxHashSet<_> =
95
+ graph.find_matching_modules(&expression).tokens().collect();
96
+
97
+ assert_eq!(
98
+ matching_modules,
99
+ case.expected_matching_modules
100
+ .into_iter()
101
+ .map(|module| graph.get_module_by_name(module).unwrap().token)
102
+ .collect()
103
+ );
104
+ }
105
+ }
@@ -1,5 +1,5 @@
1
1
  use crate::errors::GrimpResult;
2
- use crate::graph::pathfinding::{find_reach, find_shortest_path_bidirectional};
2
+ use crate::graph::pathfinding::{find_reach, find_shortest_path};
3
3
  use crate::graph::{ExtendWithDescendants, Graph, ModuleToken};
4
4
  use itertools::Itertools;
5
5
  use rustc_hash::{FxHashMap, FxHashSet};
@@ -62,7 +62,7 @@ impl Graph {
62
62
  excluded_modules: &FxHashSet<ModuleToken>,
63
63
  excluded_imports: &FxHashMap<ModuleToken, FxHashSet<ModuleToken>>,
64
64
  ) -> GrimpResult<Option<Vec<ModuleToken>>> {
65
- find_shortest_path_bidirectional(
65
+ find_shortest_path(
66
66
  self,
67
67
  from_modules,
68
68
  to_modules,
@@ -19,6 +19,8 @@ pub(crate) mod pathfinding;
19
19
  lazy_static! {
20
20
  static ref MODULE_NAMES: RwLock<StringInterner<StringBackend>> =
21
21
  RwLock::new(StringInterner::default());
22
+ static ref IMPORT_LINE_CONTENTS: RwLock<StringInterner<StringBackend>> =
23
+ RwLock::new(StringInterner::default());
22
24
  static ref EMPTY_MODULE_TOKENS: FxHashSet<ModuleToken> = FxHashSet::default();
23
25
  static ref EMPTY_IMPORT_DETAILS: FxHashSet<ImportDetails> = FxHashSet::default();
24
26
  static ref EMPTY_IMPORTS: FxHashSet<(ModuleToken, ModuleToken)> = FxHashSet::default();
@@ -32,7 +34,7 @@ pub struct Module {
32
34
  token: ModuleToken,
33
35
 
34
36
  #[getset(get_copy = "pub")]
35
- name: DefaultSymbol,
37
+ interned_name: DefaultSymbol,
36
38
 
37
39
  // Invisible modules exist in the hierarchy but haven't been explicitly added to the graph.
38
40
  #[getset(get_copy = "pub")]
@@ -43,9 +45,9 @@ pub struct Module {
43
45
  }
44
46
 
45
47
  impl Module {
46
- pub fn name_as_string(&self) -> String {
48
+ pub fn name(&self) -> String {
47
49
  let interner = MODULE_NAMES.read().unwrap();
48
- interner.resolve(self.name).unwrap().to_owned()
50
+ interner.resolve(self.interned_name).unwrap().to_owned()
49
51
  }
50
52
  }
51
53
 
@@ -102,13 +104,13 @@ pub trait ModuleIterator<'a>: Iterator<Item = &'a Module> + Sized {
102
104
  self.map(|m| m.token)
103
105
  }
104
106
 
105
- fn names(self) -> impl Iterator<Item = DefaultSymbol> {
106
- self.map(|m| m.name)
107
+ fn interned_names(self) -> impl Iterator<Item = DefaultSymbol> {
108
+ self.map(|m| m.interned_name)
107
109
  }
108
110
 
109
- fn names_as_strings(self) -> impl Iterator<Item = String> {
111
+ fn names(self) -> impl Iterator<Item = String> {
110
112
  let interner = MODULE_NAMES.read().unwrap();
111
- self.map(move |m| interner.resolve(m.name).unwrap().to_owned())
113
+ self.map(move |m| interner.resolve(m.interned_name).unwrap().to_owned())
112
114
  }
113
115
 
114
116
  fn visible(self) -> impl ModuleIterator<'a> {
@@ -129,9 +131,18 @@ impl<'a, T: Iterator<Item = &'a ModuleToken>> ModuleTokenIterator<'a> for T {}
129
131
  #[derive(Debug, Clone, PartialEq, Eq, Hash, new, Getters, CopyGetters)]
130
132
  pub struct ImportDetails {
131
133
  #[getset(get_copy = "pub")]
132
- line_number: usize,
134
+ line_number: u32,
133
135
 
134
- #[new(into)]
135
- #[getset(get = "pub")]
136
- line_contents: String,
136
+ #[getset(get_copy = "pub")]
137
+ interned_line_contents: DefaultSymbol,
138
+ }
139
+
140
+ impl ImportDetails {
141
+ pub fn line_contents(&self) -> String {
142
+ let interner = IMPORT_LINE_CONTENTS.read().unwrap();
143
+ interner
144
+ .resolve(self.interned_line_contents)
145
+ .unwrap()
146
+ .to_owned()
147
+ }
137
148
  }
@@ -28,7 +28,8 @@ pub fn find_reach(
28
28
  &seen.into_iter().collect::<FxHashSet<_>>() - from_modules
29
29
  }
30
30
 
31
- pub fn find_shortest_path_bidirectional(
31
+ /// Finds the shortest path, via a bidirectional BFS.
32
+ pub fn find_shortest_path(
32
33
  graph: &Graph,
33
34
  from_modules: &FxHashSet<ModuleToken>,
34
35
  to_modules: &FxHashSet<ModuleToken>,