scrap4rb 0.2.1 → 0.2.2

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6e95177ddee73c4a8b9c61d2b7ba4f71da0f0809f4fd321728b4212f68e195f8
4
- data.tar.gz: c457ee8fe9023e42b7388093da7ad75488155b3c19d82139f34a52043bbb43b6
3
+ metadata.gz: 6b7d1598905c975218cc1e268749cb4ab21ef2277fc28414536dcb6e8c0132fd
4
+ data.tar.gz: ca6df9174a9d75f3a3cdbef37213a2b9f596b15b6661f402c1705d674c085f7e
5
5
  SHA512:
6
- metadata.gz: c3018805b73ab90fc395558602f14c43d1a6314640008de45118f8bdc8e46b6617d4174cf81b156691ebbec0ddb85bd4fe9e77e69b9c735b94bd307d0a038058
7
- data.tar.gz: 54bf9499db12c403578b056c4eddb4a1c0de6066e7f9e32a7103dbab2c91e0ee5f979cb064446be254abf89433174b359a28c3c0090b7a8c74de3bb8c023cc47
6
+ metadata.gz: 1f858b26273076b66cfc9d2f1abde656f81a18c0cd0129a075ea9f5f33b180b0e90f4c9032562829a1417b4c19d39965982f45ecef3a947cced2e1af31631a04
7
+ data.tar.gz: d01334fa91516614bbb58a909650bb99f2acb3100741daef2bd4b66ed8bdad2f630bd60f633adb0f30e431c73ecd68750e82d2d11ef6d9e32cf9c5921c8a71fe
data/CHANGELOG.md CHANGED
@@ -6,6 +6,11 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.2.2] - 2026-11-01
10
+
11
+ ### Added
12
+ - The `symmetric-fixture` smell, penalty 3. It fires when one arrange form gives the same number or string twice, such as `Zone.new(area_m2: 50, plint_m: 50)`. A test like that cannot tell a swap of the two fields apart. It is not a SCRAP smell. See Deviations from SCRAP in the README.
13
+
9
14
  ## [0.2.1] - 2026-10-16
10
15
 
11
16
  ### Fixed
@@ -43,7 +48,8 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
43
48
  `describe`/`it` blocks. A `define_method` test inside a loop over a table
44
49
  (`CASES.each`) is one table-driven test that spans the loop.
45
50
 
46
- [Unreleased]: https://github.com/roberthopman/scrap4rb/compare/v0.2.1...HEAD
51
+ [Unreleased]: https://github.com/roberthopman/scrap4rb/compare/v0.2.2...HEAD
52
+ [0.2.2]: https://github.com/roberthopman/scrap4rb/compare/v0.2.1...v0.2.2
47
53
  [0.2.1]: https://github.com/roberthopman/scrap4rb/compare/v0.2.0...v0.2.1
48
54
  [0.2.0]: https://github.com/roberthopman/scrap4rb/compare/v0.1.0...v0.2.0
49
55
  [0.1.0]: https://github.com/roberthopman/scrap4rb/releases/tag/v0.1.0
data/README.md CHANGED
@@ -106,6 +106,7 @@ large-example more than 20 lines 4
106
106
  helper-hidden-complexity more than 8 lines hidden in local helpers 4
107
107
  temp-resource-work files, directories, threads or processes 3
108
108
  literal-heavy-setup a string over 5 lines, a hash or array over 10 3
109
+ symmetric-fixture the same value twice in one arrange fixture 3
109
110
  ```
110
111
 
111
112
  A short test with one or two assertions and few subjects is an "API contract" test. SCRAP excuses it from the low-assertion-density and large-example smells.
@@ -142,6 +143,7 @@ The policy numbers (weights, thresholds, levels) are SCRAP's, unchanged.
142
143
  - A call to an assertion helper defined in `test/test_helper.rb`, `test/application_system_test_case.rb`, `test/test_helpers/` or `test/support/` counts as one assertion. scrap4rb finds these files from the current directory. SCRAP only reads the spec file itself. Without this rule, every test that uses a shared assertion helper would be "no-assertions".
143
144
  - A loop over a literal hash (`{ 5 => 0, 6 => 10 }.each`) counts as table-driven, the same as a loop over an array of arrays.
144
145
  - `--compare` does not call new tests that use a helper a helper regression. See `docs/compare-verdict.md`.
146
+ - `symmetric-fixture` is not a SCRAP smell. It fires when one arrange form gives the same number or string twice, as in `Zone.new(area_m2: 50, plint_m: 50)`. A test like that cannot tell a swap of the two fields apart. The values `0`, `1` and `""` do not count, because a test about zero cannot pick another value. Assertions and shared `setup` blocks are not read: a repeated value in an expected result is the result, and one repeated value in a shared fixture would mark every test in the class.
145
147
  - The `where` lines show only for a file with more than one describe block or class. SCRAP shows them for each file.
146
148
  - A `describe` inside a `describe` is not a structure error, because minitest/spec nests them. A second test with the same name is a structure error. SCRAP has no such check.
147
149
 
@@ -164,7 +166,7 @@ The golden tests run the executable against the fixture suite in `test/fixtures/
164
166
 
165
167
  ## Credits
166
168
 
167
- SCRAP is by Robert C. Martin (Uncle Bob): [github.com/unclebob/scrap](https://github.com/unclebob/scrap). The algorithm, the metrics, the smells and the policy numbers in scrap4rb come from SCRAP. scrap4rb is an independent Ruby implementation. It contains no SCRAP source code.
169
+ SCRAP is by Robert C. Martin (Uncle Bob): [github.com/unclebob/scrap](https://github.com/unclebob/scrap). The algorithm, the metrics, the smells and the policy numbers in scrap4rb come from SCRAP, except the `symmetric-fixture` smell, which is listed under Deviations from SCRAP. scrap4rb is an independent Ruby implementation. It contains no SCRAP source code.
168
170
 
169
171
  scrap4rb follows SCRAP at commit [`f793b28`](https://github.com/unclebob/scrap/tree/f793b28906d3c60e30b128e1ec08df6269a72b13) (2026-03-17). When SCRAP changes its rules or numbers, scrap4rb can differ until it follows again.
170
172
 
@@ -108,7 +108,7 @@ module Scrap4rb
108
108
  m[:large_literals] <= 0 && m[:subject_symbols].size <= 4 && phases <= 1 && line_count <= 18
109
109
  end
110
110
 
111
- def smells(m, line_count, phases, table_driven, api_contract)
111
+ def smells(m, line_count, phases, table_driven, api_contract, symmetric_fixture = false)
112
112
  [
113
113
  [m[:assertions].zero?, "no-assertions", 10],
114
114
  [m[:assertions] == 1 && line_count > 10 && !table_driven && !api_contract, "low-assertion-density", 6],
@@ -117,6 +117,7 @@ module Scrap4rb
117
117
  [line_count > 20 && !api_contract, "large-example", 4],
118
118
  [m[:temp_resources].positive?, "temp-resource-work", 3],
119
119
  [m[:large_literals].positive?, "literal-heavy-setup", 3],
120
+ [symmetric_fixture, "symmetric-fixture", 3],
120
121
  [m[:helper_hidden_lines] > 8, "helper-hidden-complexity", 4]
121
122
  ].select(&:first).map { { label: _2, penalty: _3 } }
122
123
  end
@@ -131,12 +132,13 @@ module Scrap4rb
131
132
  setup_depth = api ? [0, m[:max_setup_depth] - 2].max : m[:max_setup_depth]
132
133
  branch_penalty = m[:table_driven] ? m[:table_branches] : m[:branches] + m[:table_branches]
133
134
  branch_penalty = [0, branch_penalty - 2].max if api
134
- found = smells(m, line_count, phases, m[:table_driven], api)
135
135
  complexity_score = saturating(1 + branch_penalty + setup_depth + m[:helper_calls])
136
136
 
137
137
  setup = inherited_setup + forms.select { context_head?(_1) }
138
138
  asserts = forms.select { phase(_1, ctx) == :assert }
139
139
  arrange = forms.reject { phase(_1, ctx) == :assert }
140
+ symmetric = arrange.any? { |f| [f, *descendants(f)].any? { symmetric_fixture?(_1) } }
141
+ found = smells(m, line_count, phases, m[:table_driven], api, symmetric)
140
142
  setup_signatures = form_signatures(setup)
141
143
  literal_signatures = body.flat_map { large_literal_signatures(_1) }
142
144
  {
@@ -14,6 +14,7 @@ module Scrap4rb
14
14
  BLOCK_CALLS = %i[describe context].to_set
15
15
  SETUP_CALLS = %i[setup before].to_set
16
16
  LET_CALLS = %i[let let! subject].to_set
17
+ DEGENERATE_VALUES = [0, 1, "0", "1", ""].freeze
17
18
 
18
19
  extend self
19
20
 
@@ -93,6 +94,33 @@ module Scrap4rb
93
94
  end
94
95
  end
95
96
 
97
+ def fixture_value(node)
98
+ value = case node
99
+ when Prism::IntegerNode, Prism::FloatNode then node.value
100
+ when Prism::StringNode then node.unescaped
101
+ end
102
+ value unless DEGENERATE_VALUES.include?(value)
103
+ end
104
+
105
+ def argument_groups(call)
106
+ hashes, plain = (call.arguments&.arguments || []).partition { _1.is_a?(Prism::HashNode) || _1.is_a?(Prism::KeywordHashNode) }
107
+ [plain] + hashes.map { _1.elements.grep(Prism::AssocNode).map(&:value) }
108
+ end
109
+
110
+ def repeated_fixture_values?(nodes)
111
+ values = nodes.filter_map { fixture_value(_1) }
112
+ values.size >= 2 && values.uniq.size < values.size
113
+ end
114
+
115
+ def symmetric_fixture?(node)
116
+ groups = case node
117
+ when Prism::ArrayNode then [node.elements]
118
+ when Prism::CallNode then argument_groups(node)
119
+ else []
120
+ end
121
+ groups.any? { repeated_fixture_values?(_1) }
122
+ end
123
+
96
124
  def control_call?(node)
97
125
  assertion?(node) || context_head?(node) || stub_call?(node) || TABLE_CALLS.include?(node.name) || node.name == :loop ||
98
126
  TEST_CALLS.include?(node.name) || SETUP_CALLS.include?(node.name) || !node.name.match?(/\A[a-z_]/i)
@@ -1,3 +1,3 @@
1
1
  module Scrap4rb
2
- VERSION = "0.2.1"
2
+ VERSION = "0.2.2"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: scrap4rb
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.1
4
+ version: 0.2.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Robert Hopman