diff --git a/CHANGELOG.md b/CHANGELOG.md index b629d12e..9ddb384c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,27 @@ # Changelog +## Unreleased + +### Features + +#### `pks list-references` lists the references between packs + +`pks list-references` prints, as JSON, every reference from one pack to a +constant that another pack defines, with any violations `pks check` finds in +it, recorded or not. Each violation says whether a `package_todo.yml` records +it and whether the pack enforcing it is strict. A reference that the packs' +rules allow, such as one covered by a declared dependency, is listed with no +violations. So this shows how other packs use a pack, which `package_todo.yml` +files alone can't. Unlike `check`, it exits 0 when references have violations. + +`--defining-pack` and `--referencing-pack` limit the list to references to or +from the packs they name, and each can be given more than once. Given both, a +reference must match both. + +pks keeps no source positions in ERB, so a reference in an ERB file has a null +`line` and `column`, and the file's references to one constant are listed once. +`schema/list-references-output.json` describes the output. + ## 0.6.0 ### Features diff --git a/Cargo.toml b/Cargo.toml index d2583614..6a4fd7aa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -52,7 +52,7 @@ csv = "1.3.0" # csv de/ itertools = "0.13.0" # tools for iterating over iterable things jwalk = "0.8.1" # for walking the file tree path-clean = "1.0.1" # Pathname#cleaname in Ruby -rayon = "1.7.0" # for parallel iteration +rayon = "1.10.0" # for parallel iteration regex = "1.7.3" serde = { version = "~1", features = ["derive"] } # de(serialization) yaml_serde = "0.10.6" # de(serialization) diff --git a/README.md b/README.md index 4eed9f21..38092238 100644 --- a/README.md +++ b/README.md @@ -59,6 +59,7 @@ Commands: list-packs List packs based on configuration in packwerk.yml (for debugging purposes) list-included-files List analyzed files based on configuration in packwerk.yml (for debugging purposes) list-definitions List the constants that packs sees and where it sees them (for debugging purposes) + list-references List references between packs as JSON, with any violations they make help Print this message or the help of the given subcommand(s) Options: diff --git a/schema/list-references-output.json b/schema/list-references-output.json new file mode 100644 index 00000000..a0d166b2 --- /dev/null +++ b/schema/list-references-output.json @@ -0,0 +1,76 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "pks list-references JSON output", + "type": "object", + "required": ["references"], + "additionalProperties": false, + "properties": { + "references": { + "type": "array", + "items": { "$ref": "#/$defs/Reference" }, + "description": "Sorted by file, line, column, constant_name and defining_pack_name. A constant that several packs define gives a reference to each of them at the same place." + } + }, + "$defs": { + "ViolationType": { + "type": "string", + "enum": ["dependency", "privacy", "visibility", "layer", "folder_privacy"] + }, + "Reference": { + "type": "object", + "required": [ + "file", + "line", + "column", + "constant_name", + "referencing_pack_name", + "defining_pack_name", + "violations" + ], + "additionalProperties": false, + "properties": { + "file": { + "type": "string", + "description": "Relative to the project root" + }, + "line": { + "type": ["integer", "null"], + "minimum": 1, + "description": "Counted from 1. null in ERB, where pks keeps no source positions, so an ERB file's references to one constant are listed once. `pks check -o json` gives 0 there." + }, + "column": { + "type": ["integer", "null"], + "minimum": 0, + "description": "Counted in bytes from 0. null when line is null." + }, + "constant_name": { "type": "string" }, + "referencing_pack_name": { "type": "string" }, + "defining_pack_name": { "type": "string" }, + "violations": { + "type": "array", + "items": { "$ref": "#/$defs/Violation" }, + "description": "The violations `pks check --ignore-recorded-violations` finds in this reference, in the order dependency, privacy, visibility, layer, folder_privacy. Empty when the reference breaks no rule that is enforced, as when a dependency is declared or enforce_dependencies is false." + } + }, + "if": { "properties": { "line": { "type": "null" } } }, + "then": { "properties": { "column": { "type": "null" } } }, + "else": { "properties": { "column": { "type": "integer" } } } + }, + "Violation": { + "type": "object", + "required": ["violation_type", "strict", "recorded"], + "additionalProperties": false, + "properties": { + "violation_type": { "$ref": "#/$defs/ViolationType" }, + "strict": { + "type": "boolean", + "description": "Whether the pack that enforces the rule is strict about it: the referencing pack for dependency and layer, and the defining pack for privacy, visibility and folder_privacy" + }, + "recorded": { + "type": "boolean", + "description": "Whether a package_todo.yml records this violation. `pks check` reports only the violations that are not recorded." + } + } + } + } +} diff --git a/src/packs.rs b/src/packs.rs index 33f20fd4..c0061bde 100644 --- a/src/packs.rs +++ b/src/packs.rs @@ -18,6 +18,7 @@ pub(crate) mod monkey_patch_detection; pub(crate) mod pack; pub(crate) mod parsing; pub(crate) mod raw_configuration; +pub(crate) mod reference_lister; pub(crate) mod remover; pub(crate) mod template; pub(crate) mod text; @@ -84,6 +85,14 @@ pub fn remove( remover::remove(configuration, &name, force) } +pub fn list_references( + configuration: &Configuration, + defining_packs: &[String], + referencing_packs: &[String], +) -> anyhow::Result<()> { + reference_lister::list(configuration, defining_packs, referencing_packs) +} + /// Determine whether to use colors based on the color choice fn color_mode_for(color: ColorChoice) -> text::ColorMode { match color { diff --git a/src/packs/checker.rs b/src/packs/checker.rs index 00b981b8..7cf22bdc 100644 --- a/src/packs/checker.rs +++ b/src/packs/checker.rs @@ -569,7 +569,7 @@ fn get_all_violations( violations } -fn get_checkers( +pub(crate) fn get_checkers( configuration: &Configuration, ) -> Vec> { vec![ diff --git a/src/packs/cli.rs b/src/packs/cli.rs index e2592dea..3dd23390 100644 --- a/src/packs/cli.rs +++ b/src/packs/cli.rs @@ -185,6 +185,19 @@ enum Command { about = "List the constants that packs sees and where it sees them (for debugging purposes)" )] ListDefinitions(ListDefinitionsArgs), + + #[clap( + about = "List references between packs as JSON, with any violations they make" + )] + ListReferences { + /// List only references to this pack's constants (can be repeated) + #[arg(long, value_name = "PACK")] + defining_pack: Vec, + + /// List only references from this pack's files (can be repeated) + #[arg(long, value_name = "PACK")] + referencing_pack: Vec, + }, } #[derive(ValueEnum, Copy, Clone, Debug, PartialEq, Eq)] @@ -337,6 +350,14 @@ pub fn run() -> anyhow::Result<()> { let ambiguous = args.ambiguous; packs::list_definitions(&configuration, ambiguous) } + Command::ListReferences { + defining_pack, + referencing_pack, + } => packs::list_references( + &configuration, + &defining_pack, + &referencing_pack, + ), Command::ExposeMonkeyPatches(args) => packs::expose_monkey_patches( &configuration, &args.rubydir, diff --git a/src/packs/reference_lister.rs b/src/packs/reference_lister.rs new file mode 100644 index 00000000..8027e381 --- /dev/null +++ b/src/packs/reference_lister.rs @@ -0,0 +1,149 @@ +//! `pks list-references`: every reference from one pack to a constant that +//! another pack defines, with the violations `check` finds in it, as JSON. +//! +//! See `schema/list-references-output.json` for the JSON Schema specification. + +use std::{ + collections::HashSet, + io::{BufWriter, Write}, +}; + +use rayon::prelude::{ParallelIterator, ParallelSlice, ParallelSliceMut}; +use serde::Serialize; + +use super::{ + checker::{get_checkers, reference::Reference}, + checker_configuration::CheckerType, + reference_extractor::get_all_references, + Configuration, +}; + +#[derive(Serialize)] +struct JsonOutput<'a> { + references: Vec>, +} + +#[derive(Serialize)] +struct JsonReference<'a> { + file: &'a str, + line: Option, + column: Option, + constant_name: &'a str, + referencing_pack_name: &'a str, + defining_pack_name: &'a str, + violations: Vec, +} + +#[derive(Serialize)] +struct JsonViolation { + violation_type: CheckerType, + strict: bool, + recorded: bool, +} + +pub(crate) fn list( + configuration: &Configuration, + defining_packs: &[String], + referencing_packs: &[String], +) -> anyhow::Result<()> { + let defining_packs = pack_names(configuration, defining_packs)?; + let referencing_packs = pack_names(configuration, referencing_packs)?; + + let all_references = + get_all_references(configuration, &configuration.included_files)?; + let mut references: Vec<&Reference> = all_references + .iter() + .filter(|reference| { + let Some(defining) = reference.defining_pack_name.as_deref() else { + return false; // A constant no pack defines, such as a gem's + }; + let referencing = reference.referencing_pack_name.as_str(); + defining != referencing + && selected(&defining_packs, defining) + && selected(&referencing_packs, referencing) + }) + .collect(); + // The order the schema promises. It also puts the copies of a reference + // next to each other, and as they give the same output in any order, an + // unstable sort will do. + references.par_sort_unstable_by(|a, b| identity(a).cmp(&identity(b))); + + let checkers = get_checkers(configuration); + let recorded_violations = &configuration.pack_set.all_violations; + let references = references + // A constant that a pack defines in several files yields a copy of the + // reference for each file, and their violations can differ, as when + // only one of the files is public. Like `check`, list each violation + // that any copy makes. + .par_chunk_by(|a, b| identity(a) == identity(b)) + .map(|copies| { + let mut violations = Vec::new(); + for checker in &checkers { + for reference in copies { + if let Some(violation) = + checker.check(reference, configuration)? + { + violations.push(JsonViolation { + strict: violation.strict, + recorded: recorded_violations + .contains(&violation.identifier), + violation_type: violation.identifier.violation_type, + }); + break; // One of each type, as `check` lists them + } + } + } + + let reference = copies[0]; + // pks keeps no source positions in ERB, and reports line 0 there + let location = (reference.source_location.line > 0) + .then_some(&reference.source_location); + Ok(JsonReference { + file: &reference.relative_referencing_file, + line: location.map(|location| location.line), + column: location.map(|location| location.column), + constant_name: &reference.constant_name, + referencing_pack_name: &reference.referencing_pack_name, + defining_pack_name: reference + .defining_pack_name + .as_deref() + .expect("references that no pack defines are filtered out"), + violations, + }) + }) + .collect::>>()?; + + let mut stdout = BufWriter::new(std::io::stdout().lock()); + serde_json::to_writer(&mut stdout, &JsonOutput { references })?; + stdout.flush()?; + Ok(()) +} + +/// The packs given on the command line, which must exist. +fn pack_names<'a>( + configuration: &'a Configuration, + names: &[String], +) -> anyhow::Result> { + names + .iter() + .map(|name| Ok(configuration.pack_set.for_pack(name)?.name.as_str())) + .collect() +} + +/// No packs given means every pack. +fn selected(packs: &HashSet<&str>, pack: &str) -> bool { + packs.is_empty() || packs.contains(pack) +} + +/// What sets a reference apart from the others. The copies of a reference made +/// for each file that defines its constant share it. So do all of an ERB file's +/// references to one constant, since they are all at line 0. +fn identity(reference: &Reference) -> (&str, usize, usize, &str, Option<&str>) { + ( + &reference.relative_referencing_file, + reference.source_location.line, + reference.source_location.column, + &reference.constant_name, + reference.defining_pack_name.as_deref(), + ) +} diff --git a/tests/list_references_test.rs b/tests/list_references_test.rs new file mode 100644 index 00000000..cba80cfa --- /dev/null +++ b/tests/list_references_test.rs @@ -0,0 +1,632 @@ +use assert_cmd::{assert::Assert, cargo::cargo_bin_cmd}; +use jsonschema::Validator; +use pretty_assertions::assert_eq; +use serde_json::{json, Value}; +use std::{error::Error, fs}; + +mod common; + +fn pks(fixture: &common::Fixture, args: &[&str]) -> Assert { + cargo_bin_cmd!("pks") + // `RUST_LOG` makes pks log to stderr, which `list_references` expects + // to be empty + .env_remove("RUST_LOG") + .arg("--project-root") + .arg(fixture.root()) + .args(args) + .assert() +} + +/// Runs pks with `args`, which include `list-references`, and returns its +/// output after checking it against the schema. +fn list_references(fixture: &common::Fixture, args: &[&str]) -> Value { + let stdout = pks(fixture, args) + .success() + .stderr("") + .get_output() + .stdout + .clone(); + let output = serde_json::from_slice(&stdout).expect("output is not JSON"); + validate_schema(&output); + output +} + +fn validate_schema(output: &Value) { + let schema = fs::read_to_string("schema/list-references-output.json") + .expect("Failed to read schema file"); + let schema: Value = + serde_json::from_str(&schema).expect("Schema should be valid JSON"); + let validator = + Validator::new(&schema).expect("Schema should be valid JSON Schema"); + let errors: Vec = validator + .iter_errors(output) + .map(|e| format!(" - {}", e)) + .collect(); + assert!( + errors.is_empty(), + "JSON output does not match schema:\n{}", + errors.join("\n") + ); +} + +// In simple_app, `packs/foo` references `::Bar`, which is private to +// `packs/bar` and which it doesn't depend on, and `::Baz`, which it depends on. + +fn bar_from_foo() -> Value { + json!({ + "file": "packs/foo/app/services/foo.rb", + "line": 3, + "column": 4, + "constant_name": "::Bar", + "referencing_pack_name": "packs/foo", + "defining_pack_name": "packs/bar", + "violations": [ + { + "violation_type": "dependency", + "strict": false, + "recorded": false + }, + { "violation_type": "privacy", "strict": false, "recorded": false } + ] + }) +} + +fn baz_from_foo() -> Value { + json!({ + "file": "packs/foo/app/services/foo.rb", + "line": 7, + "column": 4, + "constant_name": "::Baz", + "referencing_pack_name": "packs/foo", + "defining_pack_name": "packs/baz", + "violations": [] + }) +} + +#[test] +fn test_list_references() -> Result<(), Box> { + let fixture = common::Fixture::new("simple_app"); + + assert_eq!( + list_references(&fixture, &["list-references"]), + json!({ "references": [bar_from_foo(), baz_from_foo()] }) + ); + + Ok(()) +} + +#[test] +fn test_list_references_says_whether_violations_are_strict_and_recorded( +) -> Result<(), Box> { + // Every pack is strict. `packs/foo` records both its violations on `::Bar`, + // neither on `::Baz`, and only the privacy one on `::Qux`. + let fixture = common::Fixture::new("uses_strict_mode_partially_recorded"); + + assert_eq!( + list_references(&fixture, &["list-references"]), + json!({ + "references": [ + { + "file": "packs/foo/app/services/foo.rb", + "line": 3, + "column": 4, + "constant_name": "::Bar", + "referencing_pack_name": "packs/foo", + "defining_pack_name": "packs/bar", + "violations": [ + { + "violation_type": "dependency", + "strict": true, + "recorded": true + }, + { + "violation_type": "privacy", + "strict": true, + "recorded": true + } + ] + }, + { + "file": "packs/foo/app/services/foo.rb", + "line": 7, + "column": 4, + "constant_name": "::Baz", + "referencing_pack_name": "packs/foo", + "defining_pack_name": "packs/baz", + "violations": [ + { + "violation_type": "dependency", + "strict": true, + "recorded": false + }, + { + "violation_type": "privacy", + "strict": true, + "recorded": false + } + ] + }, + { + "file": "packs/foo/app/services/foo.rb", + "line": 11, + "column": 4, + "constant_name": "::Qux", + "referencing_pack_name": "packs/foo", + "defining_pack_name": "packs/qux", + "violations": [ + { + "violation_type": "dependency", + "strict": true, + "recorded": false + }, + { + "violation_type": "privacy", + "strict": true, + "recorded": true + } + ] + } + ] + }) + ); + + Ok(()) +} + +#[test] +fn test_list_references_to_given_packs() -> Result<(), Box> { + let fixture = common::Fixture::new("simple_app"); + + assert_eq!( + list_references( + &fixture, + &["list-references", "--defining-pack", "packs/baz"] + ), + json!({ "references": [baz_from_foo()] }) + ); + // With the trailing slash a shell can add when completing a path + assert_eq!( + list_references( + &fixture, + &[ + "list-references", + "--defining-pack", + "packs/bar/", + "--defining-pack", + "packs/baz", + ] + ), + json!({ "references": [bar_from_foo(), baz_from_foo()] }) + ); + + Ok(()) +} + +#[test] +fn test_list_references_from_given_packs() -> Result<(), Box> { + let fixture = common::Fixture::new("simple_app"); + + assert_eq!( + list_references( + &fixture, + &["list-references", "--referencing-pack", "packs/bar"] + ), + json!({ "references": [] }) + ); + assert_eq!( + list_references( + &fixture, + &[ + "list-references", + "--referencing-pack", + "packs/foo", + "--defining-pack", + "packs/bar", + ] + ), + json!({ "references": [bar_from_foo()] }) + ); + + Ok(()) +} + +#[test] +fn test_list_references_rejects_an_unknown_pack() -> Result<(), Box> +{ + let fixture = common::Fixture::new("simple_app"); + + for option in ["--defining-pack", "--referencing-pack"] { + pks(&fixture, &["list-references", option, "packs/nope"]) + .code(2) + .stdout("") + .stderr("Error: No pack found 'packs/nope'\n"); + } + + Ok(()) +} + +#[test] +fn test_list_references_leaves_out_references_within_a_pack( +) -> Result<(), Box> { + let fixture = common::Fixture::new("simple_app"); + // `packs/foo` defines `::Foo::Bar` too + fs::write( + fixture.path("packs/foo/app/services/foo_bar_user.rb"), + "class FooBarUser\n def bar\n Foo::Bar\n end\nend\n", + )?; + + assert_eq!( + list_references(&fixture, &["list-references"]), + json!({ "references": [bar_from_foo(), baz_from_foo()] }) + ); + + Ok(()) +} + +#[test] +fn test_list_references_in_erb() -> Result<(), Box> { + let fixture = common::Fixture::new("simple_app"); + fs::write( + fixture.path("packs/foo/app/views/foo.erb"), + "<%= Bar.bar %>\n

<%= Bar.bar %>

\n", + )?; + + // pks keeps no source positions in ERB, so the template's references to + // `::Bar` are listed once, with no line or column. + assert_eq!( + list_references( + &fixture, + &["list-references", "--defining-pack", "packs/bar"] + ), + json!({ + "references": [ + bar_from_foo(), + { + "file": "packs/foo/app/views/foo.erb", + "line": null, + "column": null, + "constant_name": "::Bar", + "referencing_pack_name": "packs/foo", + "defining_pack_name": "packs/bar", + "violations": [ + { + "violation_type": "dependency", + "strict": false, + "recorded": false + }, + { + "violation_type": "privacy", + "strict": false, + "recorded": false + } + ] + } + ] + }) + ); + + Ok(()) +} + +#[test] +fn test_list_references_to_a_constant_defined_in_several_files( +) -> Result<(), Box> { + let fixture = common::Fixture::new("simple_app"); + // The experimental parser resolves `::Bar` to each file that defines it. + // The one under app/public makes only a dependency violation, since it is + // public, and the one under app/services makes only a privacy violation, + // since `packs/foo` ignores dependencies on that folder. Neither copy of + // the reference makes both violations, whichever comes first. + fs::create_dir_all(fixture.path("packs/bar/app/public"))?; + fs::write( + fixture.path("packs/bar/app/public/bar.rb"), + "module Bar\n def self.public_method; end\nend\n", + )?; + fs::write( + fixture.path("packs/foo/package.yml"), + "\ +enforce_dependencies: true +enforce_privacy: true +dependencies: +- packs/baz +enforcement_globs_ignore: +- enforcements: + - dependency + ignores: + - packs/bar/app/services/** +", + )?; + + // As in `check`, the reference is listed once, with the violations it makes + // against either definition. `packs/baz/app/services/baz.rb` is empty, so + // the experimental parser finds no `::Baz` to resolve it to. + assert_eq!( + list_references( + &fixture, + &["--experimental-parser", "list-references"] + ), + json!({ "references": [bar_from_foo()] }) + ); + + Ok(()) +} + +/// A reference from `packs/foo` to a constant that `packs/bar` keeps private, +/// which breaks the same two rules as `bar_from_foo`. `None` is a position in +/// ERB. +fn to_bar_pack( + file: &str, + position: Option<(u64, u64)>, + constant_name: &str, +) -> Value { + let mut reference = bar_from_foo(); + reference["file"] = json!(file); + reference["line"] = json!(position.map(|(line, _)| line)); + reference["column"] = json!(position.map(|(_, column)| column)); + reference["constant_name"] = json!(constant_name); + reference +} + +#[test] +fn test_list_references_lists_each_use_of_a_constant_in_a_file( +) -> Result<(), Box> { + let fixture = common::Fixture::new("simple_app"); + // `::Bar` twice on one line, then at the same column on the next two + let file = "packs/foo/app/services/foo_multi.rb"; + fs::write( + fixture.path(file), + "module FooMulti\n def bar\n Bar.call(Bar.new)\n Bar\n \ + Bar\n end\nend\n", + )?; + + assert_eq!( + list_references(&fixture, &["list-references"]), + json!({ + "references": [ + bar_from_foo(), + baz_from_foo(), + to_bar_pack(file, Some((3, 4)), "::Bar"), + to_bar_pack(file, Some((3, 13)), "::Bar"), + to_bar_pack(file, Some((4, 4)), "::Bar"), + to_bar_pack(file, Some((5, 4)), "::Bar"), + ] + }) + ); + + Ok(()) +} + +#[test] +fn test_list_references_at_the_start_of_a_file() -> Result<(), Box> { + let fixture = common::Fixture::new("simple_app"); + // Line 1 and column 0 are positions, unlike ERB's line 0 + let file = "packs/foo/app/services/foo_first_line.rb"; + fs::write(fixture.path(file), "Bar.bar\n")?; + + assert_eq!( + list_references(&fixture, &["list-references"]), + json!({ + "references": [ + bar_from_foo(), + baz_from_foo(), + to_bar_pack(file, Some((1, 0)), "::Bar"), + ] + }) + ); + + Ok(()) +} + +#[test] +fn test_list_references_lists_files_in_order() -> Result<(), Box> { + let fixture = common::Fixture::new("simple_app"); + // pks reads files in no particular order, so with this many, the order it + // read them in would almost never be sorted + let names = ["a", "b", "c", "d", "e", "f", "g", "h"]; + for name in names.iter().rev() { + fs::write( + fixture.path(&format!("packs/foo/app/services/ref_{name}.rb")), + format!( + "module Ref{}\n def bar\n Bar\n end\nend\n", + name.to_uppercase() + ), + )?; + } + + let mut expected = vec![bar_from_foo(), baz_from_foo()]; + for name in names { + expected.push(to_bar_pack( + &format!("packs/foo/app/services/ref_{name}.rb"), + Some((3, 4)), + "::Bar", + )); + } + assert_eq!( + list_references(&fixture, &["list-references"]), + json!({ "references": expected }) + ); + + Ok(()) +} + +#[test] +fn test_list_references_in_erb_lists_each_file_and_constant( +) -> Result<(), Box> { + let fixture = common::Fixture::new("simple_app"); + // Both constants are `packs/bar`'s + fs::write( + fixture.path("packs/foo/app/views/foo.erb"), + "<%= Bar.bar %>\n<%= SomeConcern %>\n", + )?; + fs::write( + fixture.path("packs/foo/app/views/other.erb"), + "<%= Bar.bar %>\n", + )?; + + assert_eq!( + list_references( + &fixture, + &["list-references", "--defining-pack", "packs/bar"] + ), + json!({ + "references": [ + bar_from_foo(), + to_bar_pack("packs/foo/app/views/foo.erb", None, "::Bar"), + to_bar_pack( + "packs/foo/app/views/foo.erb", + None, + "::SomeConcern" + ), + to_bar_pack("packs/foo/app/views/other.erb", None, "::Bar"), + ] + }) + ); + + Ok(()) +} + +#[test] +fn test_list_references_to_a_constant_defined_in_several_packs( +) -> Result<(), Box> { + let fixture = common::Fixture::new("simple_app"); + // Under the experimental parser `::Bar` is now `packs/baz`'s as well, and + // `packs/foo` depends on `packs/baz`, which doesn't keep it private + fs::create_dir_all(fixture.path("packs/baz/app/models"))?; + fs::write( + fixture.path("packs/baz/app/models/bar.rb"), + "module Bar\n def self.other; end\nend\n", + )?; + + let mut bar_in_baz = bar_from_foo(); + bar_in_baz["defining_pack_name"] = json!("packs/baz"); + bar_in_baz["violations"] = json!([]); + assert_eq!( + list_references( + &fixture, + &["--experimental-parser", "list-references"] + ), + json!({ "references": [bar_from_foo(), bar_in_baz] }) + ); + + Ok(()) +} + +#[test] +fn test_list_references_says_when_a_violation_is_recorded_but_not_strict( +) -> Result<(), Box> { + // `packs/foo` doesn't depend on `packs/bar`, and its package_todo.yml + // records the dependency violation in both files that use `::Bar` + let fixture = common::Fixture::new("contains_package_todo"); + + let reference = |file: &str| { + json!({ + "file": file, + "line": 3, + "column": 4, + "constant_name": "::Bar", + "referencing_pack_name": "packs/foo", + "defining_pack_name": "packs/bar", + "violations": [ + { + "violation_type": "dependency", + "strict": false, + "recorded": true + } + ] + }) + }; + assert_eq!( + list_references(&fixture, &["list-references"]), + json!({ + "references": [ + reference("packs/foo/app/services/foo.rb"), + reference("packs/foo/app/services/other_foo.rb"), + ] + }) + ); + + Ok(()) +} + +/// One reference from `packs/baz`, and one from `packs/foos/too`, which the +/// rule allows, to `::Foo` in `packs/foos/foo` +fn from_baz_and_foos_too(violation_type: &str) -> Value { + let reference = |file: &str, referencing_pack_name: &str| { + json!({ + "file": file, + "line": 3, + "column": 4, + "constant_name": "::Foo", + "referencing_pack_name": referencing_pack_name, + "defining_pack_name": "packs/foos/foo", + "violations": [] + }) + }; + let mut from_baz = reference("packs/baz/app/services/baz.rb", "packs/baz"); + from_baz["violations"] = json!([ + { "violation_type": violation_type, "strict": false, "recorded": false } + ]); + json!({ + "references": [ + from_baz, + reference( + "packs/foos/too/app/services/ref_foo.rb", + "packs/foos/too" + ), + ] + }) +} + +#[test] +fn test_list_references_with_a_visibility_violation( +) -> Result<(), Box> { + let fixture = common::Fixture::new("visibility_violations"); + + assert_eq!( + list_references(&fixture, &["list-references"]), + from_baz_and_foos_too("visibility") + ); + + Ok(()) +} + +#[test] +fn test_list_references_with_a_folder_privacy_violation( +) -> Result<(), Box> { + let fixture = common::Fixture::new("folder_privacy_violations"); + + assert_eq!( + list_references(&fixture, &["list-references"]), + from_baz_and_foos_too("folder_privacy") + ); + + Ok(()) +} + +#[test] +fn test_list_references_with_a_layer_violation() -> Result<(), Box> { + let fixture = common::Fixture::new("layer_violations"); + + assert_eq!( + list_references(&fixture, &["list-references"]), + json!({ + "references": [{ + "file": "packs/feature_flags/app/services/feature_flags.rb", + "line": 2, + "column": 0, + "constant_name": "::Payments", + "referencing_pack_name": "packs/feature_flags", + "defining_pack_name": "packs/payments", + "violations": [ + { + "violation_type": "layer", + "strict": false, + "recorded": false + } + ] + }] + }) + ); + + Ok(()) +}