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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +48 -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/arguments_collection.rb +16 -0
- data/lib/rubyzen/collections/assignments_collection.rb +11 -0
- data/lib/rubyzen/collections/base_collection.rb +1 -0
- data/lib/rubyzen/collections/blocks_collection.rb +21 -0
- data/lib/rubyzen/collections/expressions_collection.rb +27 -0
- data/lib/rubyzen/collections/methods_collection.rb +29 -0
- data/lib/rubyzen/collections/returns_collection.rb +20 -0
- data/lib/rubyzen/core.rb +114 -0
- data/lib/rubyzen/declarations/assignment_declaration.rb +57 -0
- data/lib/rubyzen/declarations/block_declaration.rb +2 -0
- data/lib/rubyzen/declarations/call_site_declaration.rb +14 -0
- data/lib/rubyzen/declarations/expression_declaration.rb +99 -0
- data/lib/rubyzen/declarations/file_declaration.rb +1 -0
- data/lib/rubyzen/declarations/method_declaration.rb +2 -0
- data/lib/rubyzen/declarations/return_declaration.rb +51 -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/arguments_provider.rb +15 -0
- data/lib/rubyzen/providers/assignments_provider.rb +15 -0
- data/lib/rubyzen/providers/blocks_provider.rb +2 -1
- data/lib/rubyzen/providers/enclosing_blocks_provider.rb +16 -0
- data/lib/rubyzen/providers/returns_provider.rb +49 -0
- 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 +65 -10
- 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
|
|
@@ -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
|
-
#
|
|
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)
|
|
@@ -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
|
|
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
|