Skip to content

Commit a9d707d

Browse files
authored
Folder visibility checker added (#38)
* Add nested visibility checker * FolderVisibility is the new name * Checker code clarified
1 parent 3a06753 commit a9d707d

File tree

10 files changed

+318
-4
lines changed

10 files changed

+318
-4
lines changed

Gemfile.lock

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ GIT
1717
PATH
1818
remote: .
1919
specs:
20-
packwerk-extensions (0.1.7)
20+
packwerk-extensions (0.1.8)
2121
packwerk (>= 2.2.1)
2222
railties (>= 6.0.0)
2323
sorbet-runtime

README.md

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55
Currently, it ships the following checkers to help improve the boundaries between packages. These checkers are:
66
- A `privacy` checker that ensures other packages are using your package's public API
77
- A `visibility` checker that allows packages to be private except to an explicit group of other packages.
8-
- An experimental `architecture` checker that allows packages to specify their "layer" and requires that each layer only communicate with layers below it.
8+
- A `folder_visibility` checker that allows packages to their sibling packs and parent pack (to be used in an application that uses folder packs)
9+
- An `architecture` checker that allows packages to specify their "layer" and requires that each layer only communicate with layers below it.
910

1011
## Installation
1112

@@ -24,6 +25,7 @@ Alternatively, you can require individual checkers:
2425
require:
2526
- packwerk/privacy/checker
2627
- packwerk/visibility/checker
28+
- packwerk/folder_visibility/checker
2729
- packwerk/architecture/checker
2830
```
2931
@@ -87,6 +89,30 @@ visible_to:
8789
- components/other_package
8890
```
8991

92+
## Folder-Visibility Checker
93+
The folder visibility checker can be used to allow a package to be private to their sibling packs and parent packs and will create todos if used by any other package.
94+
95+
To enforce visibility for your package, set `enforce_folder_visibility` to `true` on your pack.
96+
97+
```yaml
98+
# components/merchandising/package.yml
99+
enforce_folder_visibility: true
100+
```
101+
102+
Here is an example of paths and whether their use of `packs/b/packs/e` is OK or not, assuming that protects itself via `enforce_folder_visibility`
103+
104+
```
105+
. OK (parent of parent)
106+
packs/a VIOLATION
107+
packs/b OK (parent)
108+
packs/b/packs/d OK (sibling)
109+
packs/b/packs/e ENFORCE_NESTED_VISIBILITY: TRUE
110+
packs/b/packs/e/packs/f VIOLATION
111+
packs/b/packs/e/packs/g VIOLATION
112+
packs/b/packs/h OK (sibling)
113+
packs/c VIOLATION
114+
```
115+
90116
## Architecture Checker
91117
The architecture checker can be used to enforce constraints on what can depend on what.
92118
@@ -105,3 +131,19 @@ layer: utility
105131
```
106132

107133
Now this pack can only depend on other utility packages.
134+
135+
136+
## Contributing
137+
138+
Got another checker you would like to add? Add it to this repo!
139+
140+
Please ensure these commands pass for you locally:
141+
142+
```
143+
bundle
144+
srb tc
145+
bin/rubocop
146+
bin/rake test
147+
```
148+
149+
Then, submit a PR!

lib/packwerk-extensions.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
require 'packwerk/privacy/checker'
88
require 'packwerk/visibility/checker'
9+
require 'packwerk/folder_visibility/checker'
910
require 'packwerk/architecture/checker'
1011

1112
module Packwerk
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
# typed: strict
2+
# frozen_string_literal: true
3+
4+
require 'packwerk/folder_visibility/package'
5+
require 'packwerk/folder_visibility/validator'
6+
7+
module Packwerk
8+
module FolderVisibility
9+
class Checker
10+
extend T::Sig
11+
include Packwerk::Checker
12+
13+
VIOLATION_TYPE = T.let('folder_visibility', String)
14+
15+
sig { override.returns(String) }
16+
def violation_type
17+
VIOLATION_TYPE
18+
end
19+
20+
sig do
21+
override
22+
.params(reference: Packwerk::Reference)
23+
.returns(T::Boolean)
24+
end
25+
def invalid_reference?(reference)
26+
referencing_package = reference.package
27+
referenced_package = reference.constant.package
28+
29+
return false if enforcement_disabled?(Package.from(referenced_package).enforce_folder_visibility)
30+
31+
# the root pack is parent folder of all packs, so we short-circuit this here
32+
referencing_package_is_root_pack = referencing_package.name == '.'
33+
return false if referencing_package_is_root_pack
34+
35+
packages_are_sibling_folders = Pathname.new(referenced_package.name).dirname == Pathname.new(referencing_package.name).dirname
36+
return false if packages_are_sibling_folders
37+
38+
referencing_package_is_parent_folder = Pathname.new(referenced_package.name).to_s.start_with?(referencing_package.name)
39+
return false if referencing_package_is_parent_folder
40+
41+
true
42+
end
43+
44+
sig do
45+
override
46+
.params(listed_offense: Packwerk::ReferenceOffense)
47+
.returns(T::Boolean)
48+
end
49+
def strict_mode_violation?(listed_offense)
50+
publishing_package = listed_offense.reference.constant.package
51+
publishing_package.config['enforce_folder_visibility'] == 'strict'
52+
end
53+
54+
sig do
55+
override
56+
.params(reference: Packwerk::Reference)
57+
.returns(String)
58+
end
59+
def message(reference)
60+
source_desc = "'#{reference.package}'"
61+
62+
message = <<~MESSAGE
63+
Folder Visibility violation: '#{reference.constant.name}' belongs to '#{reference.constant.package}', which is not visible to #{source_desc} as it is not a sibling pack or parent pack.
64+
Is there a different package to use instead, or should '#{reference.constant.package}' also be visible to #{source_desc}?
65+
66+
#{standard_help_message(reference)}
67+
MESSAGE
68+
69+
message.chomp
70+
end
71+
72+
private
73+
74+
sig do
75+
params(visibility_option: T.nilable(T.any(T::Boolean, String)))
76+
.returns(T::Boolean)
77+
end
78+
def enforcement_disabled?(visibility_option)
79+
[false, nil].include?(visibility_option)
80+
end
81+
82+
sig { params(reference: Reference).returns(String) }
83+
def standard_help_message(reference)
84+
standard_message = <<~MESSAGE.chomp
85+
Inference details: this is a reference to #{reference.constant.name} which seems to be defined in #{reference.constant.location}.
86+
To receive help interpreting or resolving this error message, see: https://github.com/Shopify/packwerk/blob/main/TROUBLESHOOT.md#Troubleshooting-violations
87+
MESSAGE
88+
89+
standard_message.chomp
90+
end
91+
end
92+
end
93+
end
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# typed: strict
2+
# frozen_string_literal: true
3+
4+
module Packwerk
5+
module FolderVisibility
6+
class Package < T::Struct
7+
extend T::Sig
8+
9+
const :enforce_folder_visibility, T.nilable(T.any(T::Boolean, String))
10+
11+
class << self
12+
extend T::Sig
13+
14+
sig { params(package: ::Packwerk::Package).returns(Package) }
15+
def from(package)
16+
Package.new(
17+
enforce_folder_visibility: package.config['enforce_folder_visibility']
18+
)
19+
end
20+
end
21+
end
22+
end
23+
end
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# typed: strict
2+
# frozen_string_literal: true
3+
4+
module Packwerk
5+
module FolderVisibility
6+
class Validator
7+
extend T::Sig
8+
include Packwerk::Validator
9+
10+
Result = Packwerk::Validator::Result
11+
12+
sig { override.params(package_set: PackageSet, configuration: Configuration).returns(Result) }
13+
def call(package_set, configuration)
14+
results = T.let([], T::Array[Result])
15+
16+
package_manifests_settings_for(configuration, 'enforce_folder_visibility').each do |config, setting|
17+
next if setting.nil?
18+
19+
next if [TrueClass, FalseClass].include?(setting.class) || setting == 'strict'
20+
21+
results << Result.new(
22+
ok: false,
23+
error_value: "\tInvalid 'enforce_folder_visibility' option: #{setting.inspect} in #{config.inspect}"
24+
)
25+
end
26+
27+
merge_results(results, separator: "\n---\n")
28+
end
29+
30+
sig { override.returns(T::Array[String]) }
31+
def permitted_keys
32+
%w[enforce_folder_visibility]
33+
end
34+
end
35+
end
36+
end

packwerk-extensions.gemspec

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
Gem::Specification.new do |spec|
22
spec.name = 'packwerk-extensions'
3-
spec.version = '0.1.7'
3+
spec.version = '0.1.8'
44
spec.authors = ['Gusto Engineers']
55
spec.email = ['[email protected]']
66

test/integration/extension_test.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ class ExtensionTest < Minitest::Test
2121
test 'extension is properly loaded' do
2222
use_template(:extended)
2323
Packwerk::Checker.all
24-
assert_equal(Packwerk::Checker.all.count, 4)
24+
assert_equal(Packwerk::Checker.all.count, 5)
2525
found_checker = Packwerk::Checker.all.any? do |checker|
2626
T.unsafe(checker).is_a?(Packwerk::Privacy::Checker)
2727
end
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
# typed: false
2+
# frozen_string_literal: true
3+
4+
require 'test_helper'
5+
6+
module Packwerk
7+
module FolderVisibility
8+
class CheckerTest < Minitest::Test
9+
extend T::Sig
10+
include FactoryHelper
11+
include RailsApplicationFixtureHelper
12+
13+
setup do
14+
setup_application_fixture
15+
end
16+
17+
teardown do
18+
teardown_application_fixture
19+
end
20+
21+
true_ = true
22+
23+
[
24+
# enforce, pack, referencing pack invalid? note
25+
[false, 'packs/a', 'packs/b/c/d/e/f', false, 'turned off'],
26+
[true_, 'packs/a', 'packs/b', false, 'siblings are ok'],
27+
[true_, 'packs/a', 'packs/c', false, 'siblings are ok'],
28+
[true_, 'packs/a/packs/1', 'packs/a/packs/2', false, 'siblings are ok'],
29+
[true_, 'packs/a/packs/1', 'packs/a/packs', false, 'access from parent is ok'],
30+
[true_, 'packs/a/packs/1', 'packs/a', false, 'access from parent of parent is ok'],
31+
[true_, 'packs/a/packs/1', 'packs', false, 'access from parent of parent is ok'],
32+
[true_, 'packs/a/packs/1', '.', false, 'access from root pack is ok'],
33+
34+
[true_, 'packs/a', 'packs/b/c', true_, 'not siblings or child'],
35+
[true_, 'packs/a', 'packs/b/packs/c', true_, 'not siblings or child'],
36+
[true_, 'packs/a/packs/1', 'packs/b/packs/1', true_, 'not siblings or child'],
37+
[true_, 'packs/a', 'packs/a/packs/1', true_, 'access to parent not ok'],
38+
[true_, 'packs/b', 'packs/a/packs/1', true_, 'not siblings or child']
39+
].each do |test|
40+
test "if #{test[1]} has enforce_folder_visibility: #{test[0]} than a reference from #{test[2]} is #{test[3] ? 'A VIOLATION' : 'OK'}" do
41+
source_package = Packwerk::Package.new(name: test[2])
42+
destination_package = Packwerk::Package.new(name: test[1], config: { 'enforce_folder_visibility' => test[0] })
43+
reference = build_reference(
44+
source_package: source_package,
45+
destination_package: destination_package
46+
)
47+
48+
assert_equal test[3], folder_visibility_checker.invalid_reference?(reference)
49+
end
50+
end
51+
52+
test 'provides a useful message' do
53+
assert_equal folder_visibility_checker.message(build_reference), <<~MSG.chomp
54+
Folder Visibility violation: '::SomeName' belongs to 'components/destination', which is not visible to 'components/source' as it is not a sibling pack or parent pack.
55+
Is there a different package to use instead, or should 'components/destination' also be visible to 'components/source'?
56+
57+
Inference details: this is a reference to ::SomeName which seems to be defined in some/location.rb.
58+
To receive help interpreting or resolving this error message, see: https://github.com/Shopify/packwerk/blob/main/TROUBLESHOOT.md#Troubleshooting-violations
59+
MSG
60+
end
61+
62+
private
63+
64+
sig { returns(Checker) }
65+
def folder_visibility_checker
66+
FolderVisibility::Checker.new
67+
end
68+
end
69+
end
70+
end
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
# typed: true
2+
# frozen_string_literal: true
3+
4+
require 'test_helper'
5+
6+
# make sure PrivateThing.constantize succeeds to pass the privacy validity check
7+
require 'fixtures/skeleton/components/timeline/app/models/private_thing'
8+
9+
module Packwerk
10+
module FolderVisibility
11+
class ValidatorTest < Minitest::Test
12+
extend T::Sig
13+
include ApplicationFixtureHelper
14+
include RailsApplicationFixtureHelper
15+
16+
setup do
17+
setup_application_fixture
18+
end
19+
20+
teardown do
21+
teardown_application_fixture
22+
end
23+
24+
# test 'call returns an error for invalid enforce_visibility value' do
25+
# use_template(:minimal)
26+
# merge_into_app_yaml_file('package.yml', { 'enforce_folder_visibility' => 'yes, please.' })
27+
28+
# result = validator.call(package_set, config)
29+
30+
# refute result.ok?
31+
# assert_match(/Invalid 'enforce_folder_visibility' option/, result.error_value)
32+
# end
33+
34+
test 'call returns success when enforce_folder_visibility is set to strict' do
35+
use_template(:minimal)
36+
merge_into_app_yaml_file('package.yml', { 'enforce_folder_visibility' => 'strict' })
37+
38+
result = validator.call(package_set, config)
39+
40+
assert result.ok?
41+
end
42+
43+
sig { returns(Packwerk::FolderVisibility::Validator) }
44+
def validator
45+
@validator ||= Packwerk::FolderVisibility::Validator.new
46+
end
47+
end
48+
end
49+
end

0 commit comments

Comments
 (0)