1pub mod cli;
4
5pub(crate) mod bin_locater;
7pub(crate) mod caching;
8pub(crate) mod checker;
9pub(crate) mod configuration;
10pub(crate) mod constant_resolver;
11pub(crate) mod dependencies;
12pub(crate) mod ignored;
13pub(crate) mod monkey_patch_detection;
14pub mod pack;
15pub(crate) mod parsing;
16pub(crate) mod raw_configuration;
17pub(crate) mod walk_directory;
18
19mod constant_dependencies;
20mod file_utils;
21mod logger;
22mod pack_set;
23mod package_todo;
24mod reference_extractor;
25
26use crate::packs;
27use crate::packs::pack::write_pack_to_disk;
28use crate::packs::pack::Pack;
29
30pub(crate) use self::checker::Violation;
32pub(crate) use self::pack_set::PackSet;
33pub(crate) use self::parsing::process_files_with_cache;
34pub(crate) use self::parsing::ruby::experimental::get_experimental_constant_resolver;
35pub(crate) use self::parsing::ruby::zeitwerk::get_zeitwerk_constant_resolver;
36pub(crate) use self::parsing::ParsedDefinition;
37pub(crate) use self::parsing::UnresolvedReference;
38use anyhow::bail;
39pub(crate) use configuration::Configuration;
40pub(crate) use package_todo::PackageTodo;
41
42use anyhow::Context;
44use serde::Deserialize;
45use serde::Serialize;
46use std::path::{Path, PathBuf};
47
48pub fn greet() {
49 println!("👋 Hello! Welcome to packs 📦 🔥 🎉 🌈. This tool is under construction.")
50}
51
52pub fn init(absolute_root: &Path, use_packwerk: bool) -> anyhow::Result<()> {
53 let command = if use_packwerk { "packwerk" } else { "pks" };
54 let root_package = format!("\
55# This file represents the root package of the application
56# Please validate the configuration using `{} validate` (for Rails applications) or running the auto generated
57# test case (for non-Rails projects). You can then use `{} check` to check your code.
58
59# Change to `true` to turn on dependency checks for this package
60enforce_dependencies: false
61
62# A list of this package's dependencies
63# Note that packages in this list require their own `package.yml` file
64# dependencies:
65# - \"packages/billing\"
66", command, command);
67
68 let packs_config = "\
69# See: Setting up the configuration file
70# https://github.com/Shopify/packwerk/blob/main/USAGE.md#configuring-packwerk
71
72# List of patterns for folder paths to include
73# include:
74# - \"**/*.{rb,rake,erb}\"
75
76# List of patterns for folder paths to exclude
77# exclude:
78# - \"{bin,node_modules,script,tmp,vendor}/**/*\"
79
80# Patterns to find package configuration files
81# package_paths: \"**/\"
82
83# List of custom associations, if any
84# custom_associations:
85# - \"cache_belongs_to\"
86
87# Whether or not you want the cache enabled (disabled by default)
88# cache: true
89
90# Where you want the cache to be stored (default below)
91# cache_directory: \"tmp/cache/packwerk\"
92";
93 let root_package_path = absolute_root.join("package.yml");
94 let packs_config_path = absolute_root.join(if use_packwerk {
95 "packwerk.yml"
96 } else {
97 "packs.yml"
98 });
99
100 if root_package_path.exists() {
101 println!("`{}` already exists!", root_package_path.display());
102 bail!("Could not initialize package.yml")
103 }
104 if packs_config_path.exists() {
105 println!("`{}` already exists!", packs_config_path.display());
106 bail!(format!(
107 "Could not initialize {}",
108 packs_config_path.display()
109 ))
110 }
111
112 std::fs::write(root_package_path.clone(), root_package).unwrap();
113 std::fs::write(packs_config_path.clone(), packs_config).unwrap();
114
115 println!(
116 "Created '{}' and '{}'",
117 packs_config_path.display(),
118 root_package_path.display()
119 );
120 Ok(())
121}
122
123fn create(configuration: &Configuration, name: String) -> anyhow::Result<()> {
124 let existing_pack = configuration.pack_set.for_pack(&name);
125 if existing_pack.is_ok() {
126 println!("`{}` already exists!", &name);
127 return Ok(());
128 }
129 let new_pack_path =
130 configuration.absolute_root.join(&name).join("package.yml");
131
132 let new_pack = Pack::from_contents(
133 &new_pack_path,
134 &configuration.absolute_root,
135 "enforce_dependencies: true",
136 PackageTodo::default(),
137 )?;
138
139 write_pack_to_disk(&new_pack)?;
140
141 let readme = format!(
142"Welcome to `{}`!
143
144If you're the author, please consider replacing this file with a README.md, which may contain:
145- What your pack is and does
146- How you expect people to use your pack
147- Example usage of your pack's public API and where to find it
148- Limitations, risks, and important considerations of usage
149- How to get in touch with eng and other stakeholders for questions or issues pertaining to this pack
150- What SLAs/SLOs (service level agreements/objectives), if any, your package provides
151- When in doubt, keep it simple
152- Anything else you may want to include!
153
154README.md should change as your public API changes.
155
156See https://github.com/rubyatscale/packs#readme for more info!",
157 new_pack.name
158);
159
160 let readme_path = configuration.absolute_root.join(&name).join("README.md");
161 std::fs::write(readme_path, readme).context("Failed to write README.md")?;
162
163 println!("Successfully created `{}`!", name);
164 Ok(())
165}
166
167pub fn check(
168 configuration: &Configuration,
169 files: Vec<String>,
170) -> anyhow::Result<()> {
171 let result = checker::check_all(configuration, files)
172 .context("Failed to check files")?;
173 println!("{}", result);
174 if result.has_violations() {
175 bail!("Violations found!")
176 }
177 Ok(())
178}
179
180pub fn update(configuration: &Configuration) -> anyhow::Result<()> {
181 if std::env::var("PACKS_DEBUG").is_ok() {
183 println!("Configuration: {:#?}", configuration);
184 }
185 checker::update(configuration)
186}
187
188pub fn add_dependency(
189 configuration: &Configuration,
190 from: String,
191 to: String,
192) -> anyhow::Result<()> {
193 let pack_set = &configuration.pack_set;
194
195 let from_pack = pack_set
196 .for_pack(&from)
197 .context(format!("`{}` not found", from))?;
198
199 let to_pack = pack_set
200 .for_pack(&to)
201 .context(format!("`{}` not found", to))?;
202
203 if from_pack.dependencies.contains(&to_pack.name) {
205 println!(
206 "`{}` already depends on `{}`!",
207 from_pack.name, to_pack.name
208 );
209 return Ok(());
210 }
211
212 let new_from_pack = from_pack.add_dependency(to_pack);
213
214 write_pack_to_disk(&new_from_pack)?;
215
216 let new_configuration = configuration::get(
222 &configuration.absolute_root,
223 &configuration.input_files_count,
224 )?;
225 let validation_result = packs::validate(&new_configuration);
226 if validation_result.is_err() {
227 println!("Added `{}` as a dependency to `{}`!", to, from);
228 println!("Warning: This creates a cycle!");
229 } else {
230 println!("Successfully added `{}` as a dependency to `{}`!", to, from);
231 }
232
233 Ok(())
234}
235
236pub fn list_included_files(configuration: Configuration) -> anyhow::Result<()> {
237 configuration
238 .included_files
239 .iter()
240 .for_each(|f| println!("{}", f.display()));
241 Ok(())
242}
243
244pub fn validate(configuration: &Configuration) -> anyhow::Result<()> {
245 checker::validate_all(configuration)
246}
247
248pub fn configuration(
249 project_root: PathBuf,
250 input_files_count: &usize,
251) -> anyhow::Result<Configuration> {
252 let absolute_root = project_root.canonicalize()?;
253 configuration::get(&absolute_root, input_files_count)
254}
255
256pub fn check_unnecessary_dependencies(
257 configuration: &Configuration,
258 auto_correct: bool,
259) -> anyhow::Result<()> {
260 if auto_correct {
261 checker::remove_unnecessary_dependencies(configuration)
262 } else {
263 checker::check_unnecessary_dependencies(configuration)
264 }
265}
266
267pub fn add_dependencies(
268 configuration: &Configuration,
269 pack_name: &str,
270) -> anyhow::Result<()> {
271 checker::add_all_dependencies(configuration, pack_name)
272}
273
274pub fn update_dependencies_for_constant(
275 configuration: &Configuration,
276 constant_name: &str,
277) -> anyhow::Result<()> {
278 match constant_dependencies::update_dependencies_for_constant(
279 configuration,
280 constant_name,
281 ) {
282 Ok(num_updated) => {
283 match num_updated {
284 0 => println!(
285 "No dependencies to update for constant '{}'",
286 constant_name
287 ),
288 1 => println!(
289 "Successfully updated 1 dependency for constant '{}'",
290 constant_name
291 ),
292 _ => println!(
293 "Successfully updated {} dependencies for constant '{}'",
294 num_updated, constant_name
295 ),
296 }
297 Ok(())
298 }
299 Err(err) => Err(anyhow::anyhow!(err)),
300 }
301}
302
303pub fn list(configuration: Configuration) {
304 for pack in configuration.pack_set.packs {
305 println!("{}", pack.yml.display())
306 }
307}
308
309pub fn lint_package_yml_files(
310 configuration: &Configuration,
311) -> anyhow::Result<()> {
312 for pack in &configuration.pack_set.packs {
313 write_pack_to_disk(pack)?
314 }
315 Ok(())
316}
317
318pub fn delete_cache(configuration: Configuration) {
319 let absolute_cache_dir = configuration.cache_directory;
320 if let Err(err) = std::fs::remove_dir_all(&absolute_cache_dir) {
321 eprintln!(
322 "Failed to remove {}: {}",
323 &absolute_cache_dir.display(),
324 err
325 );
326 }
327}
328
329#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)]
330pub struct ProcessedFile {
331 pub absolute_path: PathBuf,
332 pub unresolved_references: Vec<UnresolvedReference>,
333 pub definitions: Vec<ParsedDefinition>,
334
335 #[serde(default)] pub sigils: Vec<Sigil>,
337}
338
339#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)]
343pub struct Sigil {
344 pub name: String,
345 pub value: bool,
346}
347
348#[derive(Debug, PartialEq, Serialize, Deserialize, Default, Eq, Clone)]
349pub struct SourceLocation {
350 line: usize,
351 column: usize,
352}
353
354pub(crate) fn list_definitions(
355 configuration: &Configuration,
356 ambiguous: bool,
357) -> anyhow::Result<()> {
358 let constant_resolver = if configuration.experimental_parser {
359 let processed_files: Vec<ProcessedFile> = process_files_with_cache(
360 &configuration.included_files,
361 configuration.get_cache(),
362 configuration,
363 )?;
364
365 get_experimental_constant_resolver(
366 &configuration.absolute_root,
367 &processed_files,
368 &configuration.ignored_definitions,
369 )
370 } else {
371 if ambiguous {
372 bail!("Ambiguous mode is not supported for the Zeitwerk parser");
373 }
374 get_zeitwerk_constant_resolver(
375 &configuration.pack_set,
376 &configuration.constant_resolver_configuration(),
377 )
378 };
379
380 let constant_definition_map = constant_resolver
381 .fully_qualified_constant_name_to_constant_definition_map();
382
383 for (name, definitions) in constant_definition_map {
384 if ambiguous && definitions.len() == 1 {
385 continue;
386 }
387
388 for definition in definitions {
389 let relative_path = definition
390 .absolute_path_of_definition
391 .strip_prefix(&configuration.absolute_root)?;
392
393 println!("{:?} is defined at {:?}", name, relative_path);
394 }
395 }
396 Ok(())
397}
398
399fn expose_monkey_patches(
400 configuration: &Configuration,
401 rubydir: &PathBuf,
402 gemdir: &PathBuf,
403) -> anyhow::Result<()> {
404 println!(
405 "{}",
406 monkey_patch_detection::expose_monkey_patches(
407 configuration,
408 rubydir,
409 gemdir,
410 )?
411 );
412 Ok(())
413}
414
415fn list_dependencies(
416 configuration: &Configuration,
417 pack_name: String,
418) -> anyhow::Result<()> {
419 println!("Pack dependencies for {}\n", pack_name);
420 let dependencies =
421 dependencies::find_dependencies(configuration, &pack_name)?;
422 println!("Explicit ({}):", dependencies.explicit.len());
423 if dependencies.explicit.is_empty() {
424 println!("- None");
425 } else {
426 for dependency in dependencies.explicit {
427 println!("- {}", dependency);
428 }
429 }
430 println!("\nImplicit (violations) ({}):", dependencies.implicit.len());
431 if dependencies.implicit.is_empty() {
432 println!("- None");
433 } else {
434 let mut dependent_packs_with_violations =
435 dependencies.implicit.keys().collect::<Vec<_>>();
436 dependent_packs_with_violations.sort();
437 for dependent in dependent_packs_with_violations {
438 println!("- {}", dependent);
439 for (violation_type, count) in &dependencies.implicit[dependent] {
440 println!(" - {}: {}", violation_type, count);
441 }
442 }
443 }
444 Ok(())
445}
446
447#[cfg(test)]
448mod tests {
449 use super::*;
450 use std::path::PathBuf;
451
452 #[test]
453 fn test_for_file() {
454 let configuration = configuration::get(
455 PathBuf::from("tests/fixtures/simple_app")
456 .canonicalize()
457 .expect("Could not canonicalize path")
458 .as_path(),
459 &10,
460 )
461 .unwrap();
462 let absolute_file_path = configuration
463 .absolute_root
464 .join("packs/foo/app/services/foo.rb")
465 .canonicalize()
466 .expect("Could not canonicalize path");
467
468 assert_eq!(
469 String::from("packs/foo"),
470 configuration
471 .pack_set
472 .for_file(&absolute_file_path)
473 .unwrap()
474 .unwrap()
475 .name
476 )
477 }
478}