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,57 @@
1
+ module Rubyzen
2
+ module Declarations
3
+ # Represents a local-variable assignment (an +lvasgn+ node), e.g. +x = Repos::Foo.new+.
4
+ #
5
+ # @example
6
+ # assignment = method.assignments.first
7
+ # assignment.name #=> "x"
8
+ # assignment.value.constructor? #=> true
9
+ # assignment.value.constant_name #=> "Repos::Foo"
10
+ #
11
+ # NOTE: Multiple assignment (+a, b = ...+) is only partially modelled. Each target is
12
+ # surfaced as its own AssignmentDeclaration with a correct {#name}, but {#value} is +nil+:
13
+ # in the AST the right-hand side lives on the enclosing +masgn+ node, not on the per-target
14
+ # +lvasgn+, and which value each variable receives generally cannot be known statically
15
+ # (e.g. +a, b = build_pair+). If a rule ever needs to trace destructured assignments, model
16
+ # the shared source explicitly (a +multiple_assignment?+ predicate + +shared_source+) rather
17
+ # than attributing the shared right-hand side to each variable's {#value}.
18
+ #
19
+ class AssignmentDeclaration
20
+ include Rubyzen::Providers::FilePathProvider
21
+ include Rubyzen::Providers::LineNumberProvider
22
+ include Rubyzen::Providers::ClassNameProvider
23
+ include Rubyzen::Providers::SourceCodeProvider
24
+
25
+ # @return [RuboCop::AST::Node]
26
+ attr_reader :node
27
+
28
+ # @return [MethodDeclaration, BlockDeclaration]
29
+ attr_reader :parent
30
+
31
+ # @param node [RuboCop::AST::Node] the +lvasgn+ node
32
+ # @param parent [MethodDeclaration, BlockDeclaration] the enclosing declaration
33
+ def initialize(node, parent)
34
+ @node = node
35
+ @parent = parent
36
+ end
37
+
38
+ # Returns the name of the assigned local variable.
39
+ #
40
+ # @return [String] e.g. +"x"+
41
+ def name
42
+ node.children.first.to_s
43
+ end
44
+
45
+ # Returns the assigned value as an expression, or +nil+ when there is no value
46
+ # node (e.g. the per-variable targets of a multiple assignment, +a, b = foo+).
47
+ #
48
+ # @return [ExpressionDeclaration, nil]
49
+ def value
50
+ value_node = node.children[1]
51
+ return nil if value_node.nil?
52
+
53
+ ExpressionDeclaration.new(value_node, self)
54
+ end
55
+ end
56
+ end
57
+ end
@@ -17,6 +17,8 @@ module Rubyzen
17
17
  include Rubyzen::Providers::RaisesProvider
18
18
  include Rubyzen::Providers::SourceCodeProvider
19
19
  include Rubyzen::Providers::CallSiteProvider
20
+ include Rubyzen::Providers::ReturnsProvider
21
+ include Rubyzen::Providers::AssignmentsProvider
20
22
 
21
23
  # @return [RuboCop::AST::Node]
22
24
  attr_reader :node
@@ -13,6 +13,8 @@ module Rubyzen
13
13
  include Rubyzen::Providers::LineNumberProvider
14
14
  include Rubyzen::Providers::ClassNameProvider
15
15
  include Rubyzen::Providers::SourceCodeProvider
16
+ include Rubyzen::Providers::ArgumentsProvider
17
+ include Rubyzen::Providers::EnclosingBlocksProvider
16
18
 
17
19
  # @return [RuboCop::AST::Node]
18
20
  attr_reader :node
@@ -41,6 +43,18 @@ module Rubyzen
41
43
  node.receiver&.type == :const ? node.receiver.const_name : nil
42
44
  end
43
45
 
46
+ # Returns the receiver as an expression, or +nil+ for a receiverless call (e.g. +save+).
47
+ #
48
+ # Unlike {#receiver} (which returns a constant-name String), this models the receiver
49
+ # structurally — constant, constructor, or local variable.
50
+ #
51
+ # @return [Rubyzen::Declarations::ExpressionDeclaration, nil]
52
+ def receiver_expression
53
+ return nil if node.receiver.nil?
54
+
55
+ ExpressionDeclaration.new(node.receiver, self)
56
+ end
57
+
44
58
  # Returns the called method name.
45
59
  #
46
60
  # @return [String]
@@ -0,0 +1,99 @@
1
+ module Rubyzen
2
+ module Declarations
3
+ # Represents an arbitrary Ruby value-expression node — the value a method returns,
4
+ # the receiver of a call, a positional argument, the value of an assignment, and so on.
5
+ # Wraps any AST node and exposes its "kind" through predicates, so rules can ask
6
+ # structural questions without touching the raw AST.
7
+ #
8
+ # @example
9
+ # expr = call_site.receiver_expression
10
+ # expr.constructor? #=> true (for Repos::Foo.new.bar)
11
+ # expr.constant_name #=> "Repos::Foo"
12
+ #
13
+ class ExpressionDeclaration
14
+ include Rubyzen::Providers::FilePathProvider
15
+ include Rubyzen::Providers::LineNumberProvider
16
+ include Rubyzen::Providers::ClassNameProvider
17
+ include Rubyzen::Providers::SourceCodeProvider
18
+
19
+ # @return [RuboCop::AST::Node]
20
+ attr_reader :node
21
+
22
+ # @return [Object] the declaration that produced this expression
23
+ attr_reader :parent
24
+
25
+ # @param node [RuboCop::AST::Node] the value-expression node
26
+ # @param parent [Object] the declaration that produced this expression
27
+ def initialize(node, parent)
28
+ @node = node
29
+ @parent = parent
30
+ end
31
+
32
+ # Returns a short identifier: the constant, variable, or method name, falling
33
+ # back to the node type.
34
+ #
35
+ # @return [String]
36
+ def name
37
+ constant_name || local_variable_name || (method_call? ? method_name : node.type.to_s)
38
+ end
39
+
40
+ # @return [Boolean] true if the expression is a bare constant, e.g. +Repos::Foo+
41
+ def constant?
42
+ node.const_type?
43
+ end
44
+
45
+ # @return [Boolean] true if the expression references a local variable
46
+ def local_variable?
47
+ node.lvar_type?
48
+ end
49
+
50
+ # @return [Boolean] true if the expression is a method call (a +send+ node)
51
+ def method_call?
52
+ node.send_type?
53
+ end
54
+
55
+ # @return [Boolean] true if the expression is a constructor call, e.g. +Repos::Foo.new+
56
+ def constructor?
57
+ method_call? && node.method_name == :new
58
+ end
59
+
60
+ # @return [Boolean] true if the expression is a braced Hash literal with at least one pair
61
+ def hash_literal?
62
+ node.hash_type? && node.braces? && node.pairs.any?
63
+ end
64
+
65
+ # @return [Boolean] true if the expression is a symbol literal
66
+ def symbol?
67
+ node.sym_type?
68
+ end
69
+
70
+ # @return [Boolean] true if the expression is a string literal
71
+ def string?
72
+ node.str_type?
73
+ end
74
+
75
+ # Returns the constant name when the expression is a constant or constructs from one.
76
+ #
77
+ # @return [String, nil] e.g. +"Repos::Foo"+ for both +Repos::Foo+ and +Repos::Foo.new+
78
+ def constant_name
79
+ return node.const_name if constant?
80
+ return node.receiver.const_name if constructor? && node.receiver&.const_type?
81
+
82
+ nil
83
+ end
84
+
85
+ # Returns the called method name when the expression is a method call.
86
+ #
87
+ # @return [String, nil]
88
+ def method_name
89
+ node.method_name.to_s if method_call?
90
+ end
91
+
92
+ private
93
+
94
+ def local_variable_name
95
+ node.children.first.to_s if local_variable?
96
+ end
97
+ end
98
+ end
99
+ end
@@ -1,4 +1,5 @@
1
1
  module Rubyzen
2
+ # Domain objects wrapping AST nodes with high-level accessors.
2
3
  module Declarations
3
4
  # Represents a parsed Ruby source file. This is the root of the declaration
4
5
  # hierarchy — all other declarations are accessed through a FileDeclaration.
@@ -21,6 +21,8 @@ module Rubyzen
21
21
  include Rubyzen::Providers::VisibilityProvider
22
22
  include Rubyzen::Providers::RescuesProvider
23
23
  include Rubyzen::Providers::RaisesProvider
24
+ include Rubyzen::Providers::ReturnsProvider
25
+ include Rubyzen::Providers::AssignmentsProvider
24
26
 
25
27
  # @return [RuboCop::AST::Node]
26
28
  attr_reader :node
@@ -0,0 +1,51 @@
1
+ module Rubyzen
2
+ module Declarations
3
+ # Represents a single point at which a method or block yields a value: the
4
+ # implicit final expression of its body, or an explicit +return+ statement.
5
+ #
6
+ # @example
7
+ # ret = method.returns.first
8
+ # ret.explicit? #=> false
9
+ # ret.expression.hash_literal? #=> true
10
+ #
11
+ class ReturnDeclaration
12
+ include Rubyzen::Providers::FilePathProvider
13
+ include Rubyzen::Providers::LineNumberProvider
14
+ include Rubyzen::Providers::ClassNameProvider
15
+ include Rubyzen::Providers::SourceCodeProvider
16
+
17
+ # @return [RuboCop::AST::Node] the +return+ node, or the implicit final-expression node
18
+ attr_reader :node
19
+
20
+ # @return [MethodDeclaration, BlockDeclaration] the declaration that returns this value
21
+ attr_reader :parent
22
+
23
+ # @param node [RuboCop::AST::Node] the +return+ node, or the implicit final-expression node
24
+ # @param parent [MethodDeclaration, BlockDeclaration] the enclosing declaration
25
+ def initialize(node, parent)
26
+ @node = node
27
+ @parent = parent
28
+ end
29
+
30
+ # @return [Boolean] true if this is an explicit +return+ statement
31
+ def explicit?
32
+ node.return_type?
33
+ end
34
+
35
+ # @return [Boolean] true if this is the implicit final expression of the body
36
+ def implicit?
37
+ !explicit?
38
+ end
39
+
40
+ # The value expression being returned.
41
+ #
42
+ # @return [ExpressionDeclaration, nil] +nil+ for a bare +return+ with no value
43
+ def expression
44
+ value_node = explicit? ? node.children.first : node
45
+ return nil if value_node.nil?
46
+
47
+ ExpressionDeclaration.new(value_node, self)
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,184 @@
1
+ module Rubyzen
2
+ # Shared helper methods for Rubyzen's +zen_*+ / +assert_zen_*+ expectations —
3
+ # used by both the RSpec matchers ({Rubyzen::Matchers}) and the Minitest
4
+ # assertions ({Rubyzen::Assertions}).
5
+ #
6
+ # Provides utilities for normalizing exception lists, extracting item details,
7
+ # matching items against allowlist/baseline entries, and formatting failure
8
+ # messages. These methods are framework-agnostic — they operate only on plain
9
+ # declaration objects and instance variables set by the caller.
10
+ module ExpectationHelpers
11
+ # Normalizes a list of exception entries into unique, non-blank strings.
12
+ #
13
+ # @param entries [Array<String>, String, nil] raw exception entries
14
+ # @return [Array<String>] deduplicated, stripped, non-empty strings
15
+ def normalize_exception_entries(entries)
16
+ Array(entries).flatten.compact.map(&:to_s).map(&:strip).reject(&:empty?).uniq
17
+ end
18
+
19
+ # Extracts identifying details from a declaration item.
20
+ #
21
+ # @param item [Object] a declaration object (e.g., FileDeclaration, ClassDeclaration)
22
+ # @return [Hash{Symbol => String, nil}] hash with :name, :class_name, :file_path, :line
23
+ def item_details(item)
24
+ {
25
+ name: item.respond_to?(:name) ? item.name : nil,
26
+ class_name: item.respond_to?(:class_name) ? item.class_name : nil,
27
+ file_path: item.respond_to?(:file_path) ? item.file_path : 'Unknown file',
28
+ line: item.respond_to?(:line) ? item.line : nil
29
+ }
30
+ end
31
+
32
+ # Returns a list of unique identifier strings for an item, used for matching.
33
+ #
34
+ # @param item [Object] a declaration object
35
+ # @return [Array<String>] identifiers such as name, class name, file path, and file:line
36
+ def item_identifiers(item)
37
+ details = item_details(item)
38
+ identifiers = [details[:name], details[:class_name], details[:file_path]]
39
+
40
+ if details[:line]
41
+ identifiers << "#{details[:file_path]}:#{details[:line]}"
42
+ end
43
+
44
+ identifiers.compact.uniq
45
+ end
46
+
47
+ # Checks whether a given exception entry string matches an item.
48
+ #
49
+ # @param entry [String] an allowlist or baseline entry
50
+ # @param item [Object] a declaration object
51
+ # @return [Boolean] true if the entry matches the item by name, class, or path
52
+ def exception_entry_matches_item?(entry, item)
53
+ normalized_entry = entry.to_s.strip
54
+ return false if normalized_entry.empty?
55
+
56
+ details = item_details(item)
57
+ return true if item_identifiers(item).include?(normalized_entry)
58
+
59
+ file_path = details[:file_path]
60
+ file_path && (file_path.end_with?(normalized_entry) || file_path.end_with?("/#{normalized_entry}"))
61
+ end
62
+
63
+ # Classifies items into violations, baseline matches, allowlist matches,
64
+ # and detects stale entries in either list.
65
+ #
66
+ # @param subject_collection [Array, Object] items to classify
67
+ # @param allowlist [Array<String>, nil] allowed exception entries
68
+ # @param baseline [Array<String>, nil] baseline exception entries
69
+ # @return [Hash{Symbol => Array<String>}] keys: :violations, :baseline, :allowlist,
70
+ # :stale_baseline, :stale_allowlist
71
+ def classify_items(subject_collection, allowlist: nil, baseline: nil)
72
+ items = Array(subject_collection).compact
73
+ normalized_allowlist = normalize_exception_entries(allowlist)
74
+ normalized_baseline = normalize_exception_entries(baseline)
75
+ matched_baseline_entries = []
76
+ matched_allowlist_entries = []
77
+
78
+ grouped_items = items.group_by do |item|
79
+ matching_baseline_entry = normalized_baseline.find do |entry|
80
+ exception_entry_matches_item?(entry, item)
81
+ end
82
+
83
+ if matching_baseline_entry
84
+ matched_baseline_entries << matching_baseline_entry
85
+ :baseline
86
+ else
87
+ matching_allowlist_entry = normalized_allowlist.find do |entry|
88
+ exception_entry_matches_item?(entry, item)
89
+ end
90
+
91
+ if matching_allowlist_entry
92
+ matched_allowlist_entries << matching_allowlist_entry
93
+ :allowlist
94
+ else
95
+ :violations
96
+ end
97
+ end
98
+ end
99
+
100
+ classifications = {
101
+ baseline: Array(grouped_items[:baseline]).map { |item| element_name(item) },
102
+ allowlist: Array(grouped_items[:allowlist]).map { |item| element_name(item) },
103
+ violations: Array(grouped_items[:violations]).map { |item| element_name(item) }
104
+ }
105
+
106
+ classifications.merge(
107
+ stale_baseline: normalized_baseline - matched_baseline_entries.uniq,
108
+ stale_allowlist: normalized_allowlist - matched_allowlist_entries.uniq
109
+ )
110
+ end
111
+
112
+ # Formats a human-readable description of an item for failure messages.
113
+ #
114
+ # @param item [Object] a declaration object
115
+ # @return [String] formatted multi-line description
116
+ def element_name(item)
117
+ details = item_details(item)
118
+ location = [details[:file_path], details[:line]].compact.join(':')
119
+
120
+ case
121
+ when details[:name] && details[:class_name]
122
+ " - element: #{details[:name]}\n - class: #{details[:class_name]}\n - file: #{location}"
123
+ when details[:name]
124
+ " - element: #{details[:name]}\n - file: #{location}"
125
+ when details[:class_name]
126
+ " - class: #{details[:class_name]}\n - file: #{location}"
127
+ else
128
+ " - unknown element in #{location}"
129
+ end
130
+ end
131
+
132
+ # Builds a formatted string of violations and stale entries for failure output.
133
+ #
134
+ # @return [String, nil] formatted sections or nil if no classified items
135
+ def formatted_matcher_groups
136
+ return unless defined?(@classified_items) && @classified_items
137
+
138
+ sections = []
139
+
140
+ if @classified_items[:violations].any?
141
+ sections << "Violations:\n#{@classified_items[:violations].join("\n")}"
142
+ end
143
+
144
+ if @classified_items[:stale_baseline].any?
145
+ stale_entries = @classified_items[:stale_baseline].map { |entry| " - #{entry}" }
146
+ sections << "Stale baseline entries:\n#{stale_entries.join("\n")}"
147
+ end
148
+
149
+ if @classified_items[:stale_allowlist].any?
150
+ stale_entries = @classified_items[:stale_allowlist].map { |entry| " - #{entry}" }
151
+ sections << "Stale allowlist entries:\n#{stale_entries.join("\n")}"
152
+ end
153
+
154
+ sections.join("\n")
155
+ end
156
+
157
+ # Formats the failure message by combining the base message with
158
+ # custom messages and classified item details (violations, stale entries).
159
+ #
160
+ # Works unchanged in both RSpec matchers and Minitest assertions, since it
161
+ # only reads instance variables set by the caller (+@failure_message+,
162
+ # +@custom_message+, +@classified_items+).
163
+ #
164
+ # @param base_message [String] the default failure message
165
+ # @return [String] formatted failure message
166
+ def message_for_failure(base_message)
167
+ return @failure_message if @failure_message
168
+
169
+ details = formatted_matcher_groups
170
+
171
+ if @custom_message
172
+ if details && !details.empty?
173
+ "#{@custom_message}\n#{details}"
174
+ else
175
+ @custom_message
176
+ end
177
+ elsif details && !details.empty?
178
+ "#{base_message}\n#{details}"
179
+ else
180
+ base_message
181
+ end
182
+ end
183
+ end
184
+ end
@@ -1,15 +1,26 @@
1
- # Custom RSpec matcher that asserts a Rubyzen collection is empty.
2
- #
3
- # Used in architectural lint rules to verify that no items match
4
- # a forbidden pattern (e.g., no controllers call +.where+ directly).
5
- #
6
- # @example Ensure no controllers use .where
7
- # expect(controllers.all_methods.call_sites.with_name('where')).to zen_empty
8
- #
9
- # @example With a custom failure message
10
- # expect(violations).to zen_empty("Controllers should not call .where directly")
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::Matchers::MatcherHelpers
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
- # Custom RSpec matcher that asserts a block returns false for every item in a collection.
2
- #
3
- # Supports +allowlist:+ and +baseline:+ for gradual adoption, matching items
4
- # where the block returns true against exception lists.
5
- #
6
- # @example Ensure no methods have more than 5 parameters
7
- # expect(methods).to zen_false { |m| m.parameters.size > 5 }
8
- #
9
- # @example With a custom failure message
10
- # expect(controllers.all_methods.call_sites).to zen_false("Controllers must not call .where directly") { |cs| cs.name == 'where' }
11
- #
12
- # @example With a baseline for gradual adoption
13
- # expect(classes).to zen_false(baseline: ['LegacyModel']) { |k| k.lines_of_code > 200 }
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::Matchers::MatcherHelpers
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
- # Custom RSpec matcher that asserts a block returns true for every item in a collection.
2
- #
3
- # @example Ensure all methods have parameters
4
- # expect(methods).to zen_true { |m| m.parameters? }
5
- #
6
- # @example With a custom failure message
7
- # expect(services).to zen_true("All services must inherit from BaseService") { |s| s.superclass_name == 'BaseService' }
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::Matchers::MatcherHelpers
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
  require 'rubocop-ast'
2
2
 
3
3
  module Rubyzen
4
+ # Ruby source file parsing utilities.
4
5
  module Parsers
5
6
  # Singleton parser that converts Ruby source files into Rubyzen declarations
6
7
  # using RuboCop's AST processing. Results are cached via {Cache::ParseCache}.
@@ -0,0 +1,15 @@
1
+ module Rubyzen
2
+ module Providers
3
+ # Provides the arguments passed at a call site (or macro), as expressions.
4
+ module ArgumentsProvider
5
+ # @return [Rubyzen::Collections::ArgumentsCollection]
6
+ def arguments
7
+ Collections::ArgumentsCollection.new(
8
+ node.arguments.map do |argument_node|
9
+ Declarations::ExpressionDeclaration.new(argument_node, self)
10
+ end
11
+ )
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,15 @@
1
+ module Rubyzen
2
+ module Providers
3
+ # Provides local-variable assignments within a method or block.
4
+ module AssignmentsProvider
5
+ # @return [Rubyzen::Collections::AssignmentsCollection]
6
+ def assignments
7
+ Collections::AssignmentsCollection.new(
8
+ node.each_descendant(:lvasgn).map do |assignment_node|
9
+ Declarations::AssignmentDeclaration.new(assignment_node, self)
10
+ end
11
+ )
12
+ end
13
+ end
14
+ end
15
+ end
@@ -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 / {..}) within a declaration.
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,16 @@
1
+ module Rubyzen
2
+ module Providers
3
+ # Provides the chain of blocks (do..end / { }) that lexically enclose a declaration,
4
+ # innermost first.
5
+ module EnclosingBlocksProvider
6
+ # @return [Rubyzen::Collections::BlocksCollection]
7
+ def enclosing_blocks
8
+ Collections::BlocksCollection.new(
9
+ node.each_ancestor(:block).map do |block_node|
10
+ Declarations::BlockDeclaration.new(block_node, parent)
11
+ end
12
+ )
13
+ end
14
+ end
15
+ end
16
+ end