rubyzen-lint 0.1.0 → 0.3.0

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 (40) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +48 -0
  3. data/README.md +57 -9
  4. data/lib/rubyzen/assertions/assert_zen_empty.rb +51 -0
  5. data/lib/rubyzen/assertions/assert_zen_false.rb +49 -0
  6. data/lib/rubyzen/assertions/assert_zen_true.rb +49 -0
  7. data/lib/rubyzen/assertions/zen_assertions.rb +29 -0
  8. data/lib/rubyzen/cache/parse_cache.rb +1 -0
  9. data/lib/rubyzen/collections/arguments_collection.rb +16 -0
  10. data/lib/rubyzen/collections/assignments_collection.rb +11 -0
  11. data/lib/rubyzen/collections/base_collection.rb +1 -0
  12. data/lib/rubyzen/collections/blocks_collection.rb +21 -0
  13. data/lib/rubyzen/collections/expressions_collection.rb +27 -0
  14. data/lib/rubyzen/collections/methods_collection.rb +29 -0
  15. data/lib/rubyzen/collections/returns_collection.rb +20 -0
  16. data/lib/rubyzen/core.rb +114 -0
  17. data/lib/rubyzen/declarations/assignment_declaration.rb +57 -0
  18. data/lib/rubyzen/declarations/block_declaration.rb +2 -0
  19. data/lib/rubyzen/declarations/call_site_declaration.rb +14 -0
  20. data/lib/rubyzen/declarations/expression_declaration.rb +99 -0
  21. data/lib/rubyzen/declarations/file_declaration.rb +1 -0
  22. data/lib/rubyzen/declarations/method_declaration.rb +2 -0
  23. data/lib/rubyzen/declarations/return_declaration.rb +51 -0
  24. data/lib/rubyzen/expectation_helpers.rb +184 -0
  25. data/lib/rubyzen/matchers/zen_empty_matcher.rb +22 -11
  26. data/lib/rubyzen/matchers/zen_false_matcher.rb +22 -14
  27. data/lib/rubyzen/matchers/zen_true_matcher.rb +19 -8
  28. data/lib/rubyzen/minitest.rb +33 -0
  29. data/lib/rubyzen/parsers/a_s_t_parser.rb +1 -0
  30. data/lib/rubyzen/providers/arguments_provider.rb +15 -0
  31. data/lib/rubyzen/providers/assignments_provider.rb +15 -0
  32. data/lib/rubyzen/providers/blocks_provider.rb +2 -1
  33. data/lib/rubyzen/providers/enclosing_blocks_provider.rb +16 -0
  34. data/lib/rubyzen/providers/returns_provider.rb +49 -0
  35. data/lib/rubyzen/rspec.rb +29 -0
  36. data/lib/rubyzen/version.rb +2 -1
  37. data/lib/rubyzen.rb +8 -95
  38. data/rubyzen-lint.gemspec +9 -4
  39. metadata +65 -10
  40. data/lib/rubyzen/matchers/matcher_helpers.rb +0 -176
@@ -0,0 +1,49 @@
1
+ module Rubyzen
2
+ module Providers
3
+ # Provides the points at which a method or block yields a value: the implicit final
4
+ # expression of its body, plus every explicit +return+. Each is wrapped in a
5
+ # {Rubyzen::Declarations::ReturnDeclaration}, which knows how to extract its own value.
6
+ module ReturnsProvider
7
+ # @return [Rubyzen::Collections::ReturnsCollection]
8
+ def returns
9
+ Collections::ReturnsCollection.new(
10
+ return_nodes.map do |return_node|
11
+ Declarations::ReturnDeclaration.new(return_node, self)
12
+ end
13
+ )
14
+ end
15
+
16
+ # The value-expression(s) this method or block evaluates to, as a flat collection.
17
+ # A shortcut for +returns.expressions+.
18
+ #
19
+ # @return [Rubyzen::Collections::ExpressionsCollection]
20
+ def return_expressions
21
+ returns.expressions
22
+ end
23
+
24
+ private
25
+
26
+ # The return points to wrap: the implicit final expression of the body (unless it is
27
+ # itself an explicit +return+, which is collected separately to avoid double-counting),
28
+ # followed by every explicit +return+.
29
+ def return_nodes
30
+ nodes = []
31
+ final = implicit_final_node
32
+ nodes << final unless final.nil?
33
+ node.each_descendant(:return) { |return_node| nodes << return_node }
34
+ nodes
35
+ end
36
+
37
+ # The node a body implicitly evaluates to: the last statement of a multi-statement
38
+ # body (+begin+) or an explicit +begin..end+ (+kwbegin+), otherwise the body itself.
39
+ # Returns +nil+ when that node is an explicit +return+ (collected separately).
40
+ def implicit_final_node
41
+ body = node.body
42
+ return nil if body.nil?
43
+
44
+ final = body.begin_type? || body.kwbegin_type? ? body.children.last : body
45
+ final unless final.return_type?
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,29 @@
1
+ # RSpec entry point for Rubyzen.
2
+ #
3
+ # Loads the framework-agnostic core plus the RSpec matchers.
4
+ # Require it from your spec/spec_helper.rb:
5
+ #
6
+ # # Gemfile
7
+ # group :test do
8
+ # gem 'rubyzen-lint'
9
+ # gem 'rspec' # or rspec-rails
10
+ # end
11
+ #
12
+ # # spec/spec_helper.rb
13
+ # require 'rubyzen/rspec'
14
+ #
15
+ # Every RSpec project should already have the `rspec` gem.
16
+ # If it is missing we raise an error.
17
+ require_relative 'core'
18
+
19
+ begin
20
+ require 'rspec'
21
+ rescue LoadError
22
+ raise LoadError, "Rubyzen's RSpec matchers require the 'rspec' gem. " \
23
+ "Add `gem 'rspec'` to your Gemfile, or use the Minitest assertions via `require 'rubyzen/minitest'`."
24
+ end
25
+
26
+ require_relative 'expectation_helpers'
27
+ require_relative 'matchers/zen_empty_matcher'
28
+ require_relative 'matchers/zen_true_matcher'
29
+ require_relative 'matchers/zen_false_matcher'
@@ -1,3 +1,4 @@
1
1
  module Rubyzen
2
- VERSION = '0.1.0'
2
+ # @return [String] the current gem version
3
+ VERSION = '0.3.0'
3
4
  end
data/lib/rubyzen.rb CHANGED
@@ -1,98 +1,11 @@
1
- require 'rubocop-ast'
2
- require 'rspec'
3
- require 'zeitwerk'
4
-
5
- loader = Zeitwerk::Loader.for_gem
6
- loader.ignore("#{__dir__}/rubyzen/matchers")
7
- loader.ignore("#{__dir__}/rubyzen/rspec")
8
- loader.ignore("#{__dir__}/rubyzen/lint.rb")
9
- loader.ignore("#{__dir__}/rubyzen-lint.rb")
10
- loader.setup
11
-
12
- require_relative 'rubyzen/matchers/matcher_helpers'
13
- require_relative 'rubyzen/matchers/zen_empty_matcher'
14
- require_relative 'rubyzen/matchers/zen_true_matcher'
15
- require_relative 'rubyzen/matchers/zen_false_matcher'
16
-
17
- # Rubyzen is a Ruby architectural linter that lets you write lint rules as RSpec tests.
18
- # It wraps RuboCop AST to provide a high-level, easy-to-use API for enforcing architectural
19
- # rules across a codebase.
1
+ # Rubyzen entry point — loads the framework-agnostic core only.
20
2
  #
21
- # @example Basic usage
22
- # project = Rubyzen::Project.new(["/path/to/src", "/path/to/spec"])
23
- # controllers = project.files.with_paths("controllers/").classes
3
+ # `require 'rubyzen'` gives you the parsing/analysis API (Rubyzen::Project,
4
+ # declarations, collections) without any test framework attached. To write lint
5
+ # rules, require the adapter for your test framework instead:
24
6
  #
25
- # # Assert controllers don't call ActiveRecord directly
26
- # expect(controllers.all_methods.call_sites.with_name("where")).to zen_empty
7
+ # require 'rubyzen/rspec' # RSpec matchers: zen_empty / zen_true / zen_false
8
+ # require 'rubyzen/minitest' # Minitest assertions: assert_zen_empty / _true / _false
27
9
  #
28
- # @example Using auto-discovery (from project root)
29
- # project = Rubyzen::Project.new # scans app/, lib/, src/, spec/ automatically
30
- #
31
- module Rubyzen
32
- # Base error class for all Rubyzen errors.
33
- class Error < StandardError; end
34
-
35
- # Raised when a Ruby file cannot be parsed.
36
- class ParseError < Error; end
37
-
38
- # Yields the global configuration for customization.
39
- #
40
- # @example
41
- # Rubyzen.configure do |config|
42
- # config.paths = ['app', 'lib']
43
- # end
44
- def self.configure
45
- yield(configuration)
46
- end
47
-
48
- # Returns the global configuration instance.
49
- #
50
- # @return [Configuration]
51
- def self.configuration
52
- @configuration ||= Configuration.new
53
- end
54
-
55
- # Holds project path configuration with auto-discovery support.
56
- #
57
- # Resolution order:
58
- # 1. Explicit paths via {#paths=} (set via +Rubyzen.configure+)
59
- # 2. Auto-discovery of +app/+, +lib/+, +src/+, +spec/+ from +Dir.pwd+
60
- #
61
- # @example
62
- # Rubyzen.configure { |c| c.paths = ['app/models', 'app/controllers'] }
63
- # Rubyzen.configuration.project_paths #=> ["/full/path/app/models", "/full/path/app/controllers"]
64
- #
65
- class Configuration
66
- # Sets explicit paths to scan.
67
- # Relative paths are resolved against +Dir.pwd+.
68
- #
69
- # @param value [Array<String>] directories to analyze
70
- attr_writer :paths
71
-
72
- # Returns the resolved project paths.
73
- #
74
- # @return [Array<String>] absolute paths to directories to analyze
75
- def project_paths
76
- resolve_paths(@paths) || auto_discover_paths
77
- end
78
-
79
- private
80
-
81
- def resolve_paths(paths)
82
- return nil unless paths&.any?
83
-
84
- root = Dir.pwd
85
- paths.map do |path|
86
- File.expand_path(path, root)
87
- end
88
- end
89
-
90
- def auto_discover_paths
91
- root = Dir.pwd
92
- candidates = %w[app lib src spec].map { |d| File.join(root, d) }
93
- paths = candidates.select { |d| Dir.exist?(d) }
94
- paths = [root] if paths.empty?
95
- paths
96
- end
97
- end
98
- end
10
+ # Each adapter loads this core automatically
11
+ require_relative 'rubyzen/core'
data/rubyzen-lint.gemspec CHANGED
@@ -4,7 +4,7 @@ Gem::Specification.new do |spec|
4
4
  spec.name = 'rubyzen-lint'
5
5
  spec.version = Rubyzen::VERSION
6
6
  spec.authors = ['Perry Street Software']
7
- spec.summary = 'Architectural linter for Ruby — write lint rules as RSpec tests'
7
+ spec.summary = 'Architectural linter for Ruby — write lint rules as unit tests'
8
8
  spec.description = 'Rubyzen is a modern linter for Ruby that allows you to write architectural ' \
9
9
  'lint rules as unit tests. In the era of AI-generated code, it provides your ' \
10
10
  'team with deterministic guardrails to keep your codebase clean, maintainable, ' \
@@ -14,15 +14,20 @@ Gem::Specification.new do |spec|
14
14
 
15
15
  spec.required_ruby_version = '>= 3.1'
16
16
 
17
- spec.files = Dir.glob(%w[lib/**/*.rb rubyzen-lint.gemspec LICENSE README.md])
17
+ spec.files = Dir.glob(%w[lib/**/*.rb rubyzen-lint.gemspec LICENSE README.md CHANGELOG.md])
18
18
  spec.require_paths = ['lib']
19
19
 
20
20
  spec.add_dependency 'rubocop-ast', '~> 1.26'
21
21
  spec.add_dependency 'zeitwerk', '~> 2.6'
22
- spec.add_dependency 'rspec', '~> 3.12'
22
+
23
+ spec.add_development_dependency 'rspec', '~> 3.12'
24
+ spec.add_development_dependency 'minitest', '>= 5.0', '< 7.0'
25
+ spec.add_development_dependency 'rake', '~> 13.0'
23
26
 
24
27
  spec.metadata = {
25
28
  'source_code_uri' => 'https://github.com/perrystreetsoftware/Rubyzen',
26
- 'bug_tracker_uri' => 'https://github.com/perrystreetsoftware/Rubyzen/issues'
29
+ 'bug_tracker_uri' => 'https://github.com/perrystreetsoftware/Rubyzen/issues',
30
+ 'documentation_uri' => 'https://perrystreetsoftware.github.io/Rubyzen',
31
+ 'changelog_uri' => 'https://github.com/perrystreetsoftware/Rubyzen/blob/main/CHANGELOG.md'
27
32
  }
28
33
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rubyzen-lint
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Perry Street Software
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-05-15 00:00:00.000000000 Z
11
+ date: 2026-06-24 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rubocop-ast
@@ -45,27 +45,68 @@ dependencies:
45
45
  - - "~>"
46
46
  - !ruby/object:Gem::Version
47
47
  version: '3.12'
48
- type: :runtime
48
+ type: :development
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
52
  - - "~>"
53
53
  - !ruby/object:Gem::Version
54
54
  version: '3.12'
55
+ - !ruby/object:Gem::Dependency
56
+ name: minitest
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '5.0'
62
+ - - "<"
63
+ - !ruby/object:Gem::Version
64
+ version: '7.0'
65
+ type: :development
66
+ prerelease: false
67
+ version_requirements: !ruby/object:Gem::Requirement
68
+ requirements:
69
+ - - ">="
70
+ - !ruby/object:Gem::Version
71
+ version: '5.0'
72
+ - - "<"
73
+ - !ruby/object:Gem::Version
74
+ version: '7.0'
75
+ - !ruby/object:Gem::Dependency
76
+ name: rake
77
+ requirement: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - "~>"
80
+ - !ruby/object:Gem::Version
81
+ version: '13.0'
82
+ type: :development
83
+ prerelease: false
84
+ version_requirements: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - "~>"
87
+ - !ruby/object:Gem::Version
88
+ version: '13.0'
55
89
  description: Rubyzen is a modern linter for Ruby that allows you to write architectural
56
90
  lint rules as unit tests. In the era of AI-generated code, it provides your team
57
91
  with deterministic guardrails to keep your codebase clean, maintainable, and consistent
58
92
  as it grows.
59
- email:
93
+ email:
60
94
  executables: []
61
95
  extensions: []
62
96
  extra_rdoc_files: []
63
97
  files:
98
+ - CHANGELOG.md
64
99
  - LICENSE
65
100
  - README.md
66
101
  - lib/rubyzen-lint.rb
67
102
  - lib/rubyzen.rb
103
+ - lib/rubyzen/assertions/assert_zen_empty.rb
104
+ - lib/rubyzen/assertions/assert_zen_false.rb
105
+ - lib/rubyzen/assertions/assert_zen_true.rb
106
+ - lib/rubyzen/assertions/zen_assertions.rb
68
107
  - lib/rubyzen/cache/parse_cache.rb
108
+ - lib/rubyzen/collections/arguments_collection.rb
109
+ - lib/rubyzen/collections/assignments_collection.rb
69
110
  - lib/rubyzen/collections/attributes_collection.rb
70
111
  - lib/rubyzen/collections/base_collection.rb
71
112
  - lib/rubyzen/collections/blocks_collection.rb
@@ -73,6 +114,7 @@ files:
73
114
  - lib/rubyzen/collections/classes_collection.rb
74
115
  - lib/rubyzen/collections/constants_collection.rb
75
116
  - lib/rubyzen/collections/declaration_collection.rb
117
+ - lib/rubyzen/collections/expressions_collection.rb
76
118
  - lib/rubyzen/collections/file_collection.rb
77
119
  - lib/rubyzen/collections/macros_collection.rb
78
120
  - lib/rubyzen/collections/methods_collection.rb
@@ -81,11 +123,15 @@ files:
81
123
  - lib/rubyzen/collections/raises_collection.rb
82
124
  - lib/rubyzen/collections/requires_collection.rb
83
125
  - lib/rubyzen/collections/rescues_collection.rb
126
+ - lib/rubyzen/collections/returns_collection.rb
127
+ - lib/rubyzen/core.rb
128
+ - lib/rubyzen/declarations/assignment_declaration.rb
84
129
  - lib/rubyzen/declarations/attribute_declaration.rb
85
130
  - lib/rubyzen/declarations/block_declaration.rb
86
131
  - lib/rubyzen/declarations/call_site_declaration.rb
87
132
  - lib/rubyzen/declarations/class_declaration.rb
88
133
  - lib/rubyzen/declarations/constant_declaration.rb
134
+ - lib/rubyzen/declarations/expression_declaration.rb
89
135
  - lib/rubyzen/declarations/file_declaration.rb
90
136
  - lib/rubyzen/declarations/if_statement_declaration.rb
91
137
  - lib/rubyzen/declarations/macro_declaration.rb
@@ -95,19 +141,24 @@ files:
95
141
  - lib/rubyzen/declarations/raise_declaration.rb
96
142
  - lib/rubyzen/declarations/require_declaration.rb
97
143
  - lib/rubyzen/declarations/rescue_declaration.rb
144
+ - lib/rubyzen/declarations/return_declaration.rb
145
+ - lib/rubyzen/expectation_helpers.rb
98
146
  - lib/rubyzen/lint.rb
99
- - lib/rubyzen/matchers/matcher_helpers.rb
100
147
  - lib/rubyzen/matchers/zen_empty_matcher.rb
101
148
  - lib/rubyzen/matchers/zen_false_matcher.rb
102
149
  - lib/rubyzen/matchers/zen_true_matcher.rb
150
+ - lib/rubyzen/minitest.rb
103
151
  - lib/rubyzen/parsers/a_s_t_parser.rb
104
152
  - lib/rubyzen/project.rb
153
+ - lib/rubyzen/providers/arguments_provider.rb
154
+ - lib/rubyzen/providers/assignments_provider.rb
105
155
  - lib/rubyzen/providers/attributes_provider.rb
106
156
  - lib/rubyzen/providers/blocks_provider.rb
107
157
  - lib/rubyzen/providers/call_site_provider.rb
108
158
  - lib/rubyzen/providers/class_name_provider.rb
109
159
  - lib/rubyzen/providers/collection_filter_provider.rb
110
160
  - lib/rubyzen/providers/constants_provider.rb
161
+ - lib/rubyzen/providers/enclosing_blocks_provider.rb
111
162
  - lib/rubyzen/providers/file_path_provider.rb
112
163
  - lib/rubyzen/providers/if_statements_provider.rb
113
164
  - lib/rubyzen/providers/line_number_provider.rb
@@ -116,8 +167,10 @@ files:
116
167
  - lib/rubyzen/providers/raises_provider.rb
117
168
  - lib/rubyzen/providers/requires_provider.rb
118
169
  - lib/rubyzen/providers/rescues_provider.rb
170
+ - lib/rubyzen/providers/returns_provider.rb
119
171
  - lib/rubyzen/providers/source_code_provider.rb
120
172
  - lib/rubyzen/providers/visibility_provider.rb
173
+ - lib/rubyzen/rspec.rb
121
174
  - lib/rubyzen/version.rb
122
175
  - rubyzen-lint.gemspec
123
176
  homepage: https://github.com/perrystreetsoftware/Rubyzen
@@ -126,7 +179,9 @@ licenses:
126
179
  metadata:
127
180
  source_code_uri: https://github.com/perrystreetsoftware/Rubyzen
128
181
  bug_tracker_uri: https://github.com/perrystreetsoftware/Rubyzen/issues
129
- post_install_message:
182
+ documentation_uri: https://perrystreetsoftware.github.io/Rubyzen
183
+ changelog_uri: https://github.com/perrystreetsoftware/Rubyzen/blob/main/CHANGELOG.md
184
+ post_install_message:
130
185
  rdoc_options: []
131
186
  require_paths:
132
187
  - lib
@@ -141,8 +196,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
141
196
  - !ruby/object:Gem::Version
142
197
  version: '0'
143
198
  requirements: []
144
- rubygems_version: 3.0.3.1
145
- signing_key:
199
+ rubygems_version: 3.5.22
200
+ signing_key:
146
201
  specification_version: 4
147
- summary: Architectural linter for Ruby — write lint rules as RSpec tests
202
+ summary: Architectural linter for Ruby — write lint rules as unit tests
148
203
  test_files: []
@@ -1,176 +0,0 @@
1
- module Rubyzen
2
- module Matchers
3
- # Shared helper methods used by Rubyzen's custom RSpec matchers.
4
- #
5
- # Provides utilities for normalizing exception lists, extracting item
6
- # details, matching items against allowlist/baseline entries, and
7
- # formatting failure messages.
8
- module MatcherHelpers
9
- # Normalizes a list of exception entries into unique, non-blank strings.
10
- #
11
- # @param entries [Array<String>, String, nil] raw exception entries
12
- # @return [Array<String>] deduplicated, stripped, non-empty strings
13
- def normalize_exception_entries(entries)
14
- Array(entries).flatten.compact.map(&:to_s).map(&:strip).reject(&:empty?).uniq
15
- end
16
-
17
- # Extracts identifying details from a declaration item.
18
- #
19
- # @param item [Object] a declaration object (e.g., FileDeclaration, ClassDeclaration)
20
- # @return [Hash{Symbol => String, nil}] hash with :name, :class_name, :file_path, :line
21
- def item_details(item)
22
- {
23
- name: item.respond_to?(:name) ? item.name : nil,
24
- class_name: item.respond_to?(:class_name) ? item.class_name : nil,
25
- file_path: item.respond_to?(:file_path) ? item.file_path : 'Unknown file',
26
- line: item.respond_to?(:line) ? item.line : nil
27
- }
28
- end
29
-
30
- # Returns a list of unique identifier strings for an item, used for matching.
31
- #
32
- # @param item [Object] a declaration object
33
- # @return [Array<String>] identifiers such as name, class name, file path, and file:line
34
- def item_identifiers(item)
35
- details = item_details(item)
36
- identifiers = [details[:name], details[:class_name], details[:file_path]]
37
-
38
- if details[:line]
39
- identifiers << "#{details[:file_path]}:#{details[:line]}"
40
- end
41
-
42
- identifiers.compact.uniq
43
- end
44
-
45
- # Checks whether a given exception entry string matches an item.
46
- #
47
- # @param entry [String] an allowlist or baseline entry
48
- # @param item [Object] a declaration object
49
- # @return [Boolean] true if the entry matches the item by name, class, or path
50
- def exception_entry_matches_item?(entry, item)
51
- normalized_entry = entry.to_s.strip
52
- return false if normalized_entry.empty?
53
-
54
- details = item_details(item)
55
- return true if item_identifiers(item).include?(normalized_entry)
56
-
57
- file_path = details[:file_path]
58
- file_path && (file_path.end_with?(normalized_entry) || file_path.end_with?("/#{normalized_entry}"))
59
- end
60
-
61
- # Classifies items into violations, baseline matches, allowlist matches,
62
- # and detects stale entries in either list.
63
- #
64
- # @param subject_collection [Array, Object] items to classify
65
- # @param allowlist [Array<String>, nil] allowed exception entries
66
- # @param baseline [Array<String>, nil] baseline exception entries
67
- # @return [Hash{Symbol => Array<String>}] keys: :violations, :baseline, :allowlist,
68
- # :stale_baseline, :stale_allowlist
69
- def classify_items(subject_collection, allowlist: nil, baseline: nil)
70
- items = Array(subject_collection).compact
71
- normalized_allowlist = normalize_exception_entries(allowlist)
72
- normalized_baseline = normalize_exception_entries(baseline)
73
- matched_baseline_entries = []
74
- matched_allowlist_entries = []
75
-
76
- grouped_items = items.group_by do |item|
77
- matching_baseline_entry = normalized_baseline.find do |entry|
78
- exception_entry_matches_item?(entry, item)
79
- end
80
-
81
- if matching_baseline_entry
82
- matched_baseline_entries << matching_baseline_entry
83
- :baseline
84
- else
85
- matching_allowlist_entry = normalized_allowlist.find do |entry|
86
- exception_entry_matches_item?(entry, item)
87
- end
88
-
89
- if matching_allowlist_entry
90
- matched_allowlist_entries << matching_allowlist_entry
91
- :allowlist
92
- else
93
- :violations
94
- end
95
- end
96
- end
97
-
98
- classifications = {
99
- baseline: Array(grouped_items[:baseline]).map { |item| element_name(item) },
100
- allowlist: Array(grouped_items[:allowlist]).map { |item| element_name(item) },
101
- violations: Array(grouped_items[:violations]).map { |item| element_name(item) }
102
- }
103
-
104
- classifications.merge(
105
- stale_baseline: normalized_baseline - matched_baseline_entries.uniq,
106
- stale_allowlist: normalized_allowlist - matched_allowlist_entries.uniq
107
- )
108
- end
109
-
110
- # Formats a human-readable description of an item for failure messages.
111
- #
112
- # @param item [Object] a declaration object
113
- # @return [String] formatted multi-line description
114
- def element_name(item)
115
- details = item_details(item)
116
- location = [details[:file_path], details[:line]].compact.join(':')
117
-
118
- case
119
- when details[:name] && details[:class_name]
120
- " - element: #{details[:name]}\n - class: #{details[:class_name]}\n - file: #{location}"
121
- when details[:name]
122
- " - element: #{details[:name]}\n - file: #{location}"
123
- when details[:class_name]
124
- " - class: #{details[:class_name]}\n - file: #{location}"
125
- else
126
- " - unknown element in #{location}"
127
- end
128
- end
129
-
130
- # Builds a formatted string of violations and stale entries for failure output.
131
- #
132
- # @return [String, nil] formatted sections or nil if no classified items
133
- def formatted_matcher_groups
134
- return unless defined?(@classified_items) && @classified_items
135
-
136
- sections = []
137
-
138
- if @classified_items[:violations].any?
139
- sections << "Violations:\n#{@classified_items[:violations].join("\n")}"
140
- end
141
-
142
- if @classified_items[:stale_baseline].any?
143
- stale_entries = @classified_items[:stale_baseline].map { |entry| " - #{entry}" }
144
- sections << "Stale baseline entries:\n#{stale_entries.join("\n")}"
145
- end
146
-
147
- if @classified_items[:stale_allowlist].any?
148
- stale_entries = @classified_items[:stale_allowlist].map { |entry| " - #{entry}" }
149
- sections << "Stale allowlist entries:\n#{stale_entries.join("\n")}"
150
- end
151
-
152
- sections.join("\n")
153
- end
154
-
155
- def self.included(base)
156
- base.define_method(:message_for_failure) do |base_message|
157
- return @failure_message if @failure_message
158
-
159
- details = formatted_matcher_groups
160
-
161
- if @custom_message
162
- if details && !details.empty?
163
- "#{@custom_message}\n#{details}"
164
- else
165
- @custom_message
166
- end
167
- elsif details && !details.empty?
168
- "#{base_message}\n#{details}"
169
- else
170
- base_message
171
- end
172
- end
173
- end
174
- end
175
- end
176
- end