rubyzen-lint 0.1.0 → 0.2.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +24 -0
- data/README.md +57 -9
- data/lib/rubyzen/assertions/assert_zen_empty.rb +51 -0
- data/lib/rubyzen/assertions/assert_zen_false.rb +49 -0
- data/lib/rubyzen/assertions/assert_zen_true.rb +49 -0
- data/lib/rubyzen/assertions/zen_assertions.rb +29 -0
- data/lib/rubyzen/cache/parse_cache.rb +1 -0
- data/lib/rubyzen/collections/base_collection.rb +1 -0
- data/lib/rubyzen/core.rb +114 -0
- data/lib/rubyzen/declarations/file_declaration.rb +1 -0
- data/lib/rubyzen/expectation_helpers.rb +184 -0
- data/lib/rubyzen/matchers/zen_empty_matcher.rb +22 -11
- data/lib/rubyzen/matchers/zen_false_matcher.rb +22 -14
- data/lib/rubyzen/matchers/zen_true_matcher.rb +19 -8
- data/lib/rubyzen/minitest.rb +33 -0
- data/lib/rubyzen/parsers/a_s_t_parser.rb +1 -0
- data/lib/rubyzen/providers/blocks_provider.rb +2 -1
- data/lib/rubyzen/rspec.rb +29 -0
- data/lib/rubyzen/version.rb +2 -1
- data/lib/rubyzen.rb +8 -95
- data/rubyzen-lint.gemspec +9 -4
- metadata +49 -5
- data/lib/rubyzen/matchers/matcher_helpers.rb +0 -176
|
@@ -1,15 +1,26 @@
|
|
|
1
|
-
#
|
|
2
|
-
#
|
|
3
|
-
#
|
|
4
|
-
#
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
1
|
+
# @!parse
|
|
2
|
+
# module Rubyzen
|
|
3
|
+
# # Custom RSpec matchers for asserting on Rubyzen collections.
|
|
4
|
+
# module Matchers
|
|
5
|
+
# # Asserts that a Rubyzen collection is empty.
|
|
6
|
+
# #
|
|
7
|
+
# # Used in architectural lint rules to verify that no items match
|
|
8
|
+
# # a forbidden pattern (e.g., no controllers call +.where+ directly).
|
|
9
|
+
# #
|
|
10
|
+
# # @param custom_message [String, nil] optional failure message
|
|
11
|
+
# # @param allowlist [Array<String>, nil] items to permanently ignore
|
|
12
|
+
# # @param baseline [Array<String>, nil] known violations for gradual adoption
|
|
13
|
+
# #
|
|
14
|
+
# # @example Ensure no controllers use .where
|
|
15
|
+
# # expect(controllers.all_methods.call_sites.with_name('where')).to zen_empty
|
|
16
|
+
# #
|
|
17
|
+
# # @example With baseline for gradual adoption
|
|
18
|
+
# # expect(violations).to zen_empty(baseline: ['LegacyController'])
|
|
19
|
+
# def zen_empty(custom_message = nil, allowlist: nil, baseline: nil); end
|
|
20
|
+
# end
|
|
21
|
+
# end
|
|
11
22
|
RSpec::Matchers.define :zen_empty do |custom_message=nil, allowlist: nil, baseline: nil|
|
|
12
|
-
include Rubyzen::
|
|
23
|
+
include Rubyzen::ExpectationHelpers
|
|
13
24
|
|
|
14
25
|
match do |subject_collection|
|
|
15
26
|
options = custom_message.is_a?(Hash) ? custom_message : {}
|
|
@@ -1,18 +1,26 @@
|
|
|
1
|
-
#
|
|
2
|
-
#
|
|
3
|
-
#
|
|
4
|
-
#
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
# @
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
# @
|
|
13
|
-
#
|
|
1
|
+
# @!parse
|
|
2
|
+
# module Rubyzen
|
|
3
|
+
# module Matchers
|
|
4
|
+
# # Asserts that a block returns false for every item in a collection.
|
|
5
|
+
# #
|
|
6
|
+
# # Supports +allowlist:+ and +baseline:+ for gradual adoption, matching items
|
|
7
|
+
# # where the block returns true against exception lists.
|
|
8
|
+
# #
|
|
9
|
+
# # @param custom_message [String, nil] optional failure message
|
|
10
|
+
# # @param allowlist [Array<String>, nil] items to permanently ignore
|
|
11
|
+
# # @param baseline [Array<String>, nil] known violations for gradual adoption
|
|
12
|
+
# # @yield [item] block that should return false for each item
|
|
13
|
+
# #
|
|
14
|
+
# # @example Ensure no methods have more than 5 parameters
|
|
15
|
+
# # expect(methods).to zen_false { |m| m.parameters.size > 5 }
|
|
16
|
+
# #
|
|
17
|
+
# # @example With a baseline for gradual adoption
|
|
18
|
+
# # expect(classes).to zen_false(baseline: ['LegacyModel']) { |k| k.lines_of_code > 200 }
|
|
19
|
+
# def zen_false(custom_message = nil, allowlist: nil, baseline: nil, &block); end
|
|
20
|
+
# end
|
|
21
|
+
# end
|
|
14
22
|
RSpec::Matchers.define :zen_false do |custom_message=nil, allowlist: nil, baseline: nil|
|
|
15
|
-
include Rubyzen::
|
|
23
|
+
include Rubyzen::ExpectationHelpers
|
|
16
24
|
|
|
17
25
|
match do |subject_collection|
|
|
18
26
|
options = custom_message.is_a?(Hash) ? custom_message : {}
|
|
@@ -1,12 +1,23 @@
|
|
|
1
|
-
#
|
|
2
|
-
#
|
|
3
|
-
#
|
|
4
|
-
#
|
|
5
|
-
#
|
|
6
|
-
# @
|
|
7
|
-
#
|
|
1
|
+
# @!parse
|
|
2
|
+
# module Rubyzen
|
|
3
|
+
# module Matchers
|
|
4
|
+
# # Asserts that a block returns true for every item in a collection.
|
|
5
|
+
# #
|
|
6
|
+
# # @param custom_message [String, nil] optional failure message
|
|
7
|
+
# # @param allowlist [Array<String>, nil] items to permanently ignore
|
|
8
|
+
# # @param baseline [Array<String>, nil] known violations for gradual adoption
|
|
9
|
+
# # @yield [item] block that should return true for each item
|
|
10
|
+
# #
|
|
11
|
+
# # @example Ensure all methods have parameters
|
|
12
|
+
# # expect(methods).to zen_true { |m| m.parameters? }
|
|
13
|
+
# #
|
|
14
|
+
# # @example With a custom failure message
|
|
15
|
+
# # expect(services).to zen_true("All services must inherit from BaseService") { |s| s.superclass_name == 'BaseService' }
|
|
16
|
+
# def zen_true(custom_message = nil, allowlist: nil, baseline: nil, &block); end
|
|
17
|
+
# end
|
|
18
|
+
# end
|
|
8
19
|
RSpec::Matchers.define :zen_true do |custom_message=nil, allowlist: nil, baseline: nil|
|
|
9
|
-
include Rubyzen::
|
|
20
|
+
include Rubyzen::ExpectationHelpers
|
|
10
21
|
|
|
11
22
|
match do |subject_collection|
|
|
12
23
|
options = custom_message.is_a?(Hash) ? custom_message : {}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Minitest entry point for Rubyzen.
|
|
2
|
+
#
|
|
3
|
+
# Loads the framework-agnostic core plus the Minitest assertions.
|
|
4
|
+
# Require it from your test/test_helper.rb:
|
|
5
|
+
#
|
|
6
|
+
# # Gemfile
|
|
7
|
+
# group :test do
|
|
8
|
+
# gem 'rubyzen-lint'
|
|
9
|
+
# gem 'minitest'
|
|
10
|
+
# end
|
|
11
|
+
#
|
|
12
|
+
# # test/test_helper.rb
|
|
13
|
+
# require 'rubyzen/minitest'
|
|
14
|
+
#
|
|
15
|
+
# The assertions (+assert_zen_empty+, +assert_zen_true+, +assert_zen_false+) are
|
|
16
|
+
# mixed into +Minitest::Assertions+, so they are available in every Minitest test
|
|
17
|
+
# class and spec-style block automatically.
|
|
18
|
+
require_relative 'core'
|
|
19
|
+
|
|
20
|
+
begin
|
|
21
|
+
require 'minitest'
|
|
22
|
+
rescue LoadError
|
|
23
|
+
raise LoadError, "Rubyzen's Minitest assertions require the 'minitest' gem. " \
|
|
24
|
+
"Add `gem 'minitest'` to your Gemfile, or use the RSpec matchers via `require 'rubyzen/rspec'`."
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
require_relative 'assertions/zen_assertions'
|
|
28
|
+
|
|
29
|
+
# Call +include+ via +send+ so YARD's static parser doesn't try to document a
|
|
30
|
+
# mixin into the external Minitest::Assertions namespace (the constant only
|
|
31
|
+
# exists at runtime, after +require 'minitest'+, so YARD would warn). +include+
|
|
32
|
+
# is public on Module, so this is behaviourally identical to a plain call.
|
|
33
|
+
Minitest::Assertions.send(:include, Rubyzen::Assertions)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
module Rubyzen
|
|
2
|
+
# Mixins that add capabilities (call sites, blocks, attributes, etc.) to declarations.
|
|
2
3
|
module Providers
|
|
3
|
-
# Provides access to block expressions (do..end
|
|
4
|
+
# Provides access to block expressions (do..end and brace blocks) within a declaration.
|
|
4
5
|
module BlocksProvider
|
|
5
6
|
# @return [Rubyzen::Collections::BlocksCollection] collection of block declarations
|
|
6
7
|
def blocks
|
|
@@ -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'
|
data/lib/rubyzen/version.rb
CHANGED
data/lib/rubyzen.rb
CHANGED
|
@@ -1,98 +1,11 @@
|
|
|
1
|
-
|
|
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
|
-
#
|
|
22
|
-
#
|
|
23
|
-
#
|
|
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
|
-
# #
|
|
26
|
-
#
|
|
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
|
-
#
|
|
29
|
-
|
|
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
|
|
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
|
-
|
|
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.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Perry Street Software
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-06-03 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rubocop-ast
|
|
@@ -45,13 +45,47 @@ dependencies:
|
|
|
45
45
|
- - "~>"
|
|
46
46
|
- !ruby/object:Gem::Version
|
|
47
47
|
version: '3.12'
|
|
48
|
-
type: :
|
|
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
|
|
@@ -61,10 +95,15 @@ 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
|
|
69
108
|
- lib/rubyzen/collections/attributes_collection.rb
|
|
70
109
|
- lib/rubyzen/collections/base_collection.rb
|
|
@@ -81,6 +120,7 @@ files:
|
|
|
81
120
|
- lib/rubyzen/collections/raises_collection.rb
|
|
82
121
|
- lib/rubyzen/collections/requires_collection.rb
|
|
83
122
|
- lib/rubyzen/collections/rescues_collection.rb
|
|
123
|
+
- lib/rubyzen/core.rb
|
|
84
124
|
- lib/rubyzen/declarations/attribute_declaration.rb
|
|
85
125
|
- lib/rubyzen/declarations/block_declaration.rb
|
|
86
126
|
- lib/rubyzen/declarations/call_site_declaration.rb
|
|
@@ -95,11 +135,12 @@ files:
|
|
|
95
135
|
- lib/rubyzen/declarations/raise_declaration.rb
|
|
96
136
|
- lib/rubyzen/declarations/require_declaration.rb
|
|
97
137
|
- lib/rubyzen/declarations/rescue_declaration.rb
|
|
138
|
+
- lib/rubyzen/expectation_helpers.rb
|
|
98
139
|
- lib/rubyzen/lint.rb
|
|
99
|
-
- lib/rubyzen/matchers/matcher_helpers.rb
|
|
100
140
|
- lib/rubyzen/matchers/zen_empty_matcher.rb
|
|
101
141
|
- lib/rubyzen/matchers/zen_false_matcher.rb
|
|
102
142
|
- lib/rubyzen/matchers/zen_true_matcher.rb
|
|
143
|
+
- lib/rubyzen/minitest.rb
|
|
103
144
|
- lib/rubyzen/parsers/a_s_t_parser.rb
|
|
104
145
|
- lib/rubyzen/project.rb
|
|
105
146
|
- lib/rubyzen/providers/attributes_provider.rb
|
|
@@ -118,6 +159,7 @@ files:
|
|
|
118
159
|
- lib/rubyzen/providers/rescues_provider.rb
|
|
119
160
|
- lib/rubyzen/providers/source_code_provider.rb
|
|
120
161
|
- lib/rubyzen/providers/visibility_provider.rb
|
|
162
|
+
- lib/rubyzen/rspec.rb
|
|
121
163
|
- lib/rubyzen/version.rb
|
|
122
164
|
- rubyzen-lint.gemspec
|
|
123
165
|
homepage: https://github.com/perrystreetsoftware/Rubyzen
|
|
@@ -126,6 +168,8 @@ licenses:
|
|
|
126
168
|
metadata:
|
|
127
169
|
source_code_uri: https://github.com/perrystreetsoftware/Rubyzen
|
|
128
170
|
bug_tracker_uri: https://github.com/perrystreetsoftware/Rubyzen/issues
|
|
171
|
+
documentation_uri: https://perrystreetsoftware.github.io/Rubyzen
|
|
172
|
+
changelog_uri: https://github.com/perrystreetsoftware/Rubyzen/blob/main/CHANGELOG.md
|
|
129
173
|
post_install_message:
|
|
130
174
|
rdoc_options: []
|
|
131
175
|
require_paths:
|
|
@@ -144,5 +188,5 @@ requirements: []
|
|
|
144
188
|
rubygems_version: 3.0.3.1
|
|
145
189
|
signing_key:
|
|
146
190
|
specification_version: 4
|
|
147
|
-
summary: Architectural linter for Ruby — write lint rules as
|
|
191
|
+
summary: Architectural linter for Ruby — write lint rules as unit tests
|
|
148
192
|
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
|