crap4ruby 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: b2e848e88bdf6cd4f6b2f0a5477aa3a9f3565651006864d7101ad13beab82c7c
4
- data.tar.gz: cc9a67ce5aaea92c6d7311f00526900017df5904c157d16764b77cbd27a0a9e7
3
+ metadata.gz: 28f7ade7b1a7064708117a46abf1e555d777493d5be570fc28cf6958b196e253
4
+ data.tar.gz: 3ae6bf461495ff0e5fa2edc3ecfa773f7c20f920fb3d6f7e92f9ce42bfec3ddd
5
5
  SHA512:
6
- metadata.gz: 546293a428b456b48c4c22b34fe09980a2c88f1c7af0d69f8aa24f598fa5441993e8c50f1dc29dc346c233e0a8ffada8106fc13894a15a6185b1877c9823840a
7
- data.tar.gz: ed047a7174bcce565ff2b248729f4faa61208f8a20794a6903d0fe9cfca7e1381243bd12287502dd5cfdef1087153c0720b363e098e39e32364262ade1501ddf
6
+ metadata.gz: da1e756e8824c99bd46cb34d5f994e95689866a760870bd13f5eb3530573c5eed5c4d6903f4e60a35c16f9f8f2bcb153af2db6712d9fa4d373d8a0c27f9b7a9d
7
+ data.tar.gz: 9fc11d38f195dea8de1a7b52079e8fd1b32979bc28d12149d7ded251d887577fa26ea0ffc28b6839aa4c41fc76b906f161b7b73bb9f7f53fed4fd1b7873f5e4f
data/CHANGELOG.md CHANGED
@@ -5,6 +5,16 @@ Version numbers follow [Semantic Versioning 2.0.0](https://semver.org/spec/v2.0.
5
5
 
6
6
  ## [Unreleased]
7
7
 
8
+ ## [0.2.2] - 2026-10-29
9
+
10
+ ### Added
11
+
12
+ - Automatically analyze large selections with bounded worker processes. Add `--jobs N` to set the worker count or use `--jobs 1` for serial analysis, with serial fallback on unsupported or multithreaded hosts.
13
+
14
+ ### Changed
15
+
16
+ - Reduce analyzer, coverage-loading, discovery, scoring, and table-formatting overhead while preserving analysis results and report output. See [PERFORMANCE.md](PERFORMANCE.md) for real-project measurements.
17
+
8
18
  ## [0.2.1] - 2026-10-29
9
19
 
10
20
  ### Added
@@ -48,7 +58,8 @@ Version numbers follow [Semantic Versioning 2.0.0](https://semver.org/spec/v2.0.
48
58
 
49
59
  - Split CLI execution, AST discovery, and coverage loading into smaller methods without changing their behavior or scoring rules.
50
60
 
51
- [Unreleased]: https://github.com/svyatov/crap4ruby/compare/v0.2.1...HEAD
61
+ [Unreleased]: https://github.com/svyatov/crap4ruby/compare/v0.2.2...HEAD
62
+ [0.2.2]: https://github.com/svyatov/crap4ruby/compare/v0.2.1...v0.2.2
52
63
  [0.2.1]: https://github.com/svyatov/crap4ruby/compare/v0.2.0...v0.2.1
53
64
  [0.2.0]: https://github.com/svyatov/crap4ruby/compare/v0.1.1...v0.2.0
54
65
  [0.1.1]: https://github.com/svyatov/crap4ruby/compare/v0.1.0...v0.1.1
data/PERFORMANCE.md ADDED
@@ -0,0 +1,166 @@
1
+ # Performance
2
+
3
+ The benchmark compares complete CLI runs on local project checkouts. It checks report equality and rejects changed inputs.
4
+
5
+ ## Measured results (2026-10-29)
6
+
7
+ Baseline: `19942c2` (0.2.1). CLI candidate: `2f23c80`. Final EDN allocation follow-up: `3dd909d`, measured separately below. Runtime: Ruby 4.0.9, Prism 1.9.0, arm64 macOS.
8
+
9
+ Each variant has seven measured subprocess runs after one excluded warmup. Variant order alternates, and runs execute serially with warm filesystem caches.
10
+
11
+ | Project | Mode | Ruby files | Baseline median | Candidate median | Time reduction |
12
+ | --- | --- | ---: | ---: | ---: | ---: |
13
+ | Les | Complexity only | 196 | 0.2530 s | 0.1485 s | 41.3% |
14
+ | Les | SimpleCov coverage | 196 | 0.2840 s | 0.1788 s | 37.1% |
15
+ | Forem | Complexity only | 1,307 | 0.9631 s | 0.3761 s | 60.9% |
16
+ | Steep | Complexity only | 146 | 0.5330 s | 0.2185 s | 59.0% |
17
+ | YARD | Complexity only | 207 | 0.3699 s | 0.1768 s | 52.2% |
18
+
19
+ [Raw samples and input/output hashes](benchmark/results/2026-10-29-batch.jsonl) retain every measured duration. Each line is a JSON object. Campaign records distinguish the original baseline, the optimized serial baseline, current-code worker settings, and small inputs. Grouped samples factor identical metadata without dropping values. Phase, resource, and independent QA records are separate.
20
+
21
+ The earlier campaigns measured [candidate `80484fd`](benchmark/results/2026-10-29.jsonl), [candidate `2f35bdd`](benchmark/results/2026-10-29-follow-up.jsonl), and [candidate `6a1701b`](benchmark/results/2026-10-29-report-score.jsonl) against the same baseline. Project input and report hashes match across campaigns. Timing differences between campaigns do not isolate individual changes.
22
+
23
+ Timing includes Ruby startup, Prism activation, discovery, source reads, analysis, coverage loading, sorting, formatting, output capture, and process completion. Both variants start without inherited Bundler setup and activate the same Prism version. Runs use `--no-save`, so timing excludes snapshot serialization and writes. Test-suite execution is excluded.
24
+
25
+ Every measured report matches its baseline byte for byte. Separate differential checks preserve all fields and traversal order across 10,568 method definitions in these four projects. Independent fixture checks cover nested definitions, heredocs, encoding, assignment syntax, errors, and coverage on Ruby 3.3.11 and 4.0.9. The batch implementation also passed 75 independent CLI runs and 21 controlled lifecycle probes on Ruby 3.3.11, 3.4.9, and 4.0.9. All used Prism 1.9.0. Prism 1.0 and 2.x were not executed.
26
+
27
+ These results describe local checkouts and warm filesystem caches. They do not establish cold-disk latency, unique physical memory use, or performance on other machines. Separately sampled summed RSS is reported below.
28
+
29
+ ## Repeat the comparison
30
+
31
+ 1. Create a scratch directory with `mktemp -d`. Replace `BASELINE_DIR` below with its printed path.
32
+ 2. Export the baseline library:
33
+
34
+ ```sh
35
+ git archive --output=BASELINE_DIR/baseline.tar 19942c2 lib
36
+ ```
37
+
38
+ 3. Extract the archive:
39
+
40
+ ```sh
41
+ tar -xf BASELINE_DIR/baseline.tar -C BASELINE_DIR
42
+ ```
43
+
44
+ 4. Compare against a project checkout:
45
+
46
+ ```sh
47
+ bundle exec ruby script/benchmark.rb BASELINE_DIR/lib /path/to/project
48
+ ```
49
+
50
+ The command prints JSON to stdout and median/range summaries to stderr. Pass multiple project paths to measure them serially. Set `SAMPLES` to an odd integer of at least three to change the sample count.
51
+
52
+ For a comparison of current-code serial execution against its automatic default, use `BASELINE_JOBS=1` and pass `lib` as the baseline directory. `CANDIDATE_JOBS` can override the candidate worker count. Both optional settings are recorded in the output; use them only with library versions that support `--jobs`.
53
+
54
+ The runner selects existing `app/` and `lib/` directories. It measures complexity-only mode and adds coverage-enabled mode when `coverage/.resultset.json` exists. It hashes selected paths and contents before and after each subprocess. Coverage-enabled runs also hash the coverage report. Library hashes must remain unchanged throughout the campaign.
55
+
56
+ ## Optimization evidence
57
+
58
+ The analyzer shares one ordered result array across discovery, uses symbol dispatch for decision types, and resolves coverage endpoints by byte offset. Complexity-only CLI runs omit coverage line collection. Default library calls retain body lines.
59
+
60
+ Analysis-only comparisons against the baseline preserved complete method records and reports. They reduced median analysis time by 18% to 25% before omitting coverage collection. The omitted walk then reduced complete complexity-only CLI latency by another 16% to 23% against that intermediate implementation.
61
+
62
+ Preliminary CLI timings inherited Bundler's candidate version preload and were replaced by the clean-boot measurements above. The newer Prism child iterator was excluded because the minimum supported Prism version lacks it.
63
+
64
+ ### Follow-up experiments
65
+
66
+ Seven trials per variant alternated execution order. Each trial started a separate Ruby process and ran one excluded warmup batch. Phase timing excludes library loading and result hashing. Allocation counts use `GC.stat(:total_allocated_objects)` deltas.
67
+
68
+ | Experiment | Input and batch | Baseline median | Candidate median | Baseline allocated objects | Candidate allocated objects |
69
+ | --- | --- | ---: | ---: | ---: | ---: |
70
+ | Iteration membership hash | Forem, one complexity-only analysis | 0.4041 s | 0.3911 s | 944,945 | 945,015 |
71
+ | Direct definition-boundary checks | Forem, one analysis with body lines | 0.5592 s | 0.5284 s | 1,374,461 | 1,186,622 |
72
+ | Copy initial coverage counts | Les, 50 report loads | 0.3416 s | 0.2707 s | 1,434,813 | 87,463 |
73
+ | Skip repeated file-type checks | Forem, 20 discovery calls | 1.8376 s | 1.6761 s | 1,061,220 | 993,820 |
74
+
75
+ Each phase isolates one change against its preceding implementation. Analysis includes source reads and parsing. Coverage loading includes JSON parsing, validation, and normalization. Its input is the real Les report with 209 files and 13,399 line entries. The loader experiment screened an equivalent conditional form before the committed guard clause.
76
+
77
+ Complete method records, merged coverage data, and selected file paths match within every phase. Independent checks also matched 382 Analyzer fixtures, 176 coverage cases, and 40 discovery scenarios on Ruby 3.3.11 and 4.0.9. Coverage checks include caller-array mutation, merged suites, unequal arrays, and symlink aliases.
78
+
79
+ Moving the iteration block check before membership lookup showed no clear gain and was reverted.
80
+
81
+ ### Formatting and scoring
82
+
83
+ The table formatter now uses one format template per report. Scoring combines integer terms before constructing the exact Rational result. Both changes preserve output and reduce allocated objects.
84
+
85
+ | Experiment | Input and batch | Baseline median | Candidate median | Baseline allocated objects | Candidate allocated objects |
86
+ | --- | --- | ---: | ---: | ---: | ---: |
87
+ | Table format template | Forem, 20 tables of 5,765 entries | 0.1991 s | 0.1206 s | 1,384,240 | 461,740 |
88
+ | Integer score terms | Les, 20 scoring passes over 1,045 methods | 0.3267 s | 0.3192 s | 397,060 | 250,020 |
89
+
90
+ Each experiment has seven alternating trials per variant, with one excluded warmup batch per process. The table experiment compares `562581a` with the formatter in `c0f10f6`. The scoring experiment compares `c0f10f6` with `6a1701b`. Allocation counts cover the measured batch only.
91
+
92
+ The formatter reduced phase time by 39% and allocated objects by 67%. Scoring reduced allocated objects by 37%. Its elapsed time changed by 2.3%, and CPU time changed from 0.3256 s to 0.3175 s. These small timing changes do not establish a reliable speed gain. Median process peak RSS was 47,398,912 versus 47,431,680 bytes. Median GC count was 17 for both variants; candidate batches triggered 16 to 17 collections. Median GC time was 12 versus 10 ms. Fewer allocations are a retained improvement even without a demonstrated reduction in peak memory or median collection count.
93
+
94
+ Independent differential checks matched 146 formatter cases and 74 scoring cases on Ruby 3.3.11 and 4.0.9. These include encoding and control characters, exact Rational values, partial coverage, and large integers.
95
+
96
+ #### EDN allocation follow-up
97
+
98
+ Commit `3dd909d` trims trailing zeros in the newly formatted number string in place. This avoids copying that string. Each variant runs 100 EDN formatting passes after 100 excluded warmup passes, in seven fresh subprocesses with alternating order. Timing excludes library loading, analysis, sorting, and snapshot writes. The baseline is `af2f770`.
99
+
100
+ | Input | Baseline median | Candidate median | Baseline allocated objects | Candidate allocated objects |
101
+ | --- | ---: | ---: | ---: | ---: |
102
+ | Les, 1,045 methods with coverage | 0.1696 s | 0.1699 s | 1,701,309 | 1,517,509 |
103
+ | Forem, 5,765 methods without coverage | 0.3711 s | 0.3687 s | 2,894,109 | 2,894,109 |
104
+
105
+ Les report allocations decreased by 10.8%. Forem has no numeric coverage or scores to trim, so its allocation count is unchanged. Timing ranges overlap for both inputs; these results establish no elapsed-time improvement. All EDN hashes match, and all selected source and coverage hashes remain unchanged. [Raw samples and QA evidence](benchmark/results/2026-10-29-edn.jsonl) retain every duration and allocation count. Differential checks preserve 4,012 numeric cases on each of Ruby 3.3.11, 3.4.9, and 4.0.9. Additional EDN checks cover escaping, empty reports, exact Rational values, negative zero, and nonfinite floats on Ruby 3.3.11 and 3.4.9.
106
+
107
+ ### Automatic batch analysis
108
+
109
+ Selections of at least 64 files use up to four worker processes, bounded by available processors. Smaller selections, unsupported fork platforms, and hosts with other live Ruby threads remain serial. `--jobs 1` forces serial execution. Result order and all report writes remain controlled by the parent.
110
+
111
+ The following campaign compares the same `2f23c80` library with `--jobs 1` against its automatic default. It isolates worker selection without changing the loaded library. Each mode has seven alternating measured subprocess runs after one excluded warmup.
112
+
113
+ | Project | Mode | Serial median | Automatic median | Time reduction |
114
+ | --- | --- | ---: | ---: | ---: |
115
+ | Les | Complexity only | 0.1960 s | 0.1579 s | 19.4% |
116
+ | Les | SimpleCov coverage | 0.2438 s | 0.1800 s | 26.1% |
117
+ | Forem | Complexity only | 0.6207 s | 0.3822 s | 38.4% |
118
+ | Steep | Complexity only | 0.3215 s | 0.2160 s | 32.8% |
119
+ | YARD | Complexity only | 0.2507 s | 0.1732 s | 30.9% |
120
+
121
+ A separate paired campaign against the earlier optimized serial implementation, `6a1701b`, measured reductions of 19.6% to 40.0%. All reports matched. The raw data keeps this comparison separate from the original-baseline and worker-override campaigns.
122
+
123
+ Real-project subsets support the 64-file automatic threshold. The phase experiment includes source reads, parsing, scoring, fork startup, cancellation-thread startup, and result transfer. It excludes discovery, coverage loading, sorting, formatting, and process startup. Each trial has one excluded warmup batch; worker order rotates across seven fresh subprocess trials.
124
+
125
+ | Project | Selected files | Serial phase median | Four-worker phase median |
126
+ | --- | ---: | ---: | ---: |
127
+ | Forem | 16 | 0.0063 s | 0.0087 s |
128
+ | Forem | 32 | 0.0124 s | 0.0114 s |
129
+ | Forem | 64 | 0.0272 s | 0.0198 s |
130
+ | Forem | 128 | 0.0439 s | 0.0241 s |
131
+ | Les, coverage | 16 | 0.0068 s | 0.0116 s |
132
+ | Les, coverage | 32 | 0.0174 s | 0.0157 s |
133
+ | Les, coverage | 64 | 0.0301 s | 0.0197 s |
134
+ | Les, coverage | 128 | 0.0648 s | 0.0307 s |
135
+
136
+ Four workers lose at 16 files, show smaller gains at 32, and reduce phase time by 27% to 35% at 64. On the full selections, automatic analysis-and-scoring time was 0.1599 s versus 0.4082 s for Forem, and 0.0575 s versus 0.1186 s for Les. Median total parent-plus-child CPU time increased from 0.4073 s to 0.4615 s on Forem, and from 0.1180 s to 0.1608 s on Les: increases of 13.3% and 36.2%.
137
+
138
+ Seven alternating memory probes per configuration sampled the sum of CLI parent and worker RSS every 10 ms. This includes startup and reporting; fork telemetry and polling perturb timing, so these probes support no latency claim. Shared pages are counted once per process, and sampling can miss short peaks. These are not measurements of unique physical memory.
139
+
140
+ | Project | Serial median sampled peak sum RSS | Automatic median sampled peak sum RSS |
141
+ | --- | ---: | ---: |
142
+ | Forem | 73,678,848 bytes | 194,854,912 bytes |
143
+ | Les, coverage | 47,005,696 bytes | 112,443,392 bytes |
144
+
145
+ Independent QA compared serial baseline `6a1701b` with automatic mode and `--jobs 1/2/4` through actual CLI subprocesses on all four projects. It preserved stdout, stderr, exit statuses, source and coverage inputs, and existing snapshots across three Ruby versions. Controlled probes exercised ordering, exact Rational scores, Unicode, payloads larger than a pipe buffer, malformed results, abnormal exits, first-error ordering, multithreaded fallback, cancellation, and custom SIGCHLD reapers. All probes finished without remaining children. Cleanup made no PID signals and did not replace host signal handlers. The main suite additionally covers unavailable fork, partial startup failure, and SIGINT cancellation.
146
+
147
+ Review found a stale-PID signaling race when a host reaped worker statuses. The repaired implementation cancels through private pipes and uses a child monitor thread, then validates complete result payloads even if the host has consumed the exit status. The two reproducing regression tests and the full 86-test suite pass. The measurements above include this repair.
148
+
149
+ A separate [process-group SIGINT probe](benchmark/results/2026-10-29-batch-sigint.json) delivered SIGINT after all four workers started. The CLI terminated on signal 2, preserved its existing snapshot, emitted no report, and left no processes in its group.
150
+
151
+ Small-input CLI comparisons against `6a1701b` remained serial. Medians for 16 and 32 files ranged from effectively unchanged to 4.0% slower, with overlapping timing ranges. The largest median difference was 5.1 ms. A follow-up rejected deferred loading of `Etc`: `Tempfile` loads it transitively, and repeated load and CLI measurements showed no clear benefit. [The rejected experiment](benchmark/results/2026-10-29-startup-screen.json) retains all samples and its prototype description. EDN serialization was screened separately above. These measurements do not establish a performance maximum.
152
+
153
+ ### Batch screening prototype
154
+
155
+ An earlier throwaway fork-based prototype screened analysis and scoring across files before the production worker implementation. These historical phase measurements exclude CLI startup, discovery, sorting, formatting, and saving. Seven measured rounds followed one excluded warmup round; worker order rotated. Selected file contents and all returned entries matched across worker counts. The prototype samples are retained in the earlier report-and-score campaign.
156
+
157
+ | Project and mode | Worker processes | Median elapsed time | Median total CPU time |
158
+ | --- | ---: | ---: | ---: |
159
+ | Forem, complexity only | 1 | 0.4104 s | 0.4059 s |
160
+ | Forem, complexity only | 2 | 0.2656 s | 0.4365 s |
161
+ | Forem, complexity only | 4 | 0.1504 s | 0.4507 s |
162
+ | Les, coverage | 1 | 0.1220 s | 0.1211 s |
163
+ | Les, coverage | 2 | 0.0776 s | 0.1377 s |
164
+ | Les, coverage | 4 | 0.0566 s | 0.1532 s |
165
+
166
+ Four workers reduced prototype phase latency by 63% on Forem and 54% on Les, while increasing total CPU time by 11% and 26%. Total CPU includes parent and child processes. The production implementation and its resource costs are measured separately above.
data/README.md CHANGED
@@ -46,7 +46,7 @@ Run these commands from this checkout with Ruby 3.3 or newer:
46
46
  ```sh
47
47
  bundle install
48
48
  gem build crap4ruby.gemspec
49
- gem install ./crap4ruby-0.2.1.gem
49
+ gem install ./crap4ruby-0.2.2.gem
50
50
  ```
51
51
 
52
52
  To use Bundler in the measured project, add this checkout to that project's `Gemfile`:
@@ -97,6 +97,7 @@ crap4ruby does not configure SimpleCov, delete reports, install dependencies, or
97
97
  | `--coverage-command COMMAND` | Run a shell command that refreshes the selected report. |
98
98
  | `--no-coverage` | Report complexity with unknown coverage and CRAP. |
99
99
  | `--no-save` | Print results without creating or replacing `.metrics/crap.edn`. |
100
+ | `--jobs N` | Set a positive worker count; `1` forces serial analysis. The default selects workers automatically. |
100
101
  | `--threshold NUMBER` | Check this finite, nonnegative limit; exit 2 for a measured violation, or 1 if an unknown score could exceed it. |
101
102
  | `-h`, `--help` | Print usage. |
102
103
  | `-v`, `--version` | Print the gem version. |
@@ -126,6 +127,8 @@ bundle exec crap4ruby --no-coverage --no-save
126
127
  `--no-save` preserves the terminal report and threshold exit codes while leaving the snapshot unchanged.
127
128
  It also works with coverage enabled.
128
129
 
130
+ Selections of at least 64 files automatically use up to four worker processes, limited by the available processors. Workers reduce analysis latency by using more CPU and memory. Use `--jobs 1` to keep analysis in one process, or set a larger count explicitly. The count never exceeds the selected file count. Platforms without fork support and hosts with other live Ruby threads use serial analysis. Worker startup resource failures also fall back to serial analysis. Reports, scores, and snapshot behavior are the same in both modes.
131
+
129
132
  ## Complexity and coverage
130
133
 
131
134
  The scoring engine uses this formula, with coverage expressed as a fraction between 0 and 1:
@@ -17,7 +17,14 @@ module Crap4Ruby
17
17
  repeated_combination select! sort! each_key each_pair each_value
18
18
  fetch fetch_values has_key? merge merge! transform_keys transform_keys!
19
19
  transform_values transform_values!
20
- ].freeze
20
+ ].to_h { |name| [name, true] }.freeze
21
+
22
+ # Configures whether analysis collects body lines for coverage scoring.
23
+ #
24
+ # @param coverage [Boolean] collect body lines; false leaves MethodInfo#lines nil
25
+ def initialize(coverage: true)
26
+ @coverage = coverage
27
+ end
21
28
 
22
29
  # Analyzes method bodies with Prism and the RuboCop 1.92.1 counting rules.
23
30
  #
@@ -44,52 +51,57 @@ module Crap4Ruby
44
51
  # @param scope [Array<String>] lexical namespace components
45
52
  # @param singleton [Boolean] whether definitions belong to a singleton class
46
53
  # @param path [String] source label
54
+ # @param methods [Array<MethodInfo>] ordered output shared by the traversal
47
55
  # @return [Array<MethodInfo>] discovered method definitions
48
- def discover(node, scope, singleton, path)
49
- return [] unless node
56
+ def discover(node, scope, singleton, path, methods = [])
57
+ return methods unless node
50
58
 
51
59
  case node
52
60
  when Prism::ClassNode, Prism::ModuleNode
53
- discover_class(node, scope, singleton, path)
61
+ discover_class(node, scope, singleton, path, methods)
54
62
  when Prism::SingletonClassNode
55
63
  owner = node.expression.is_a?(Prism::SelfNode) ? scope : qualify(node.expression, scope)
56
- owner ? discover(node.body, owner, true, path) : []
64
+ discover(node.body, owner, true, path, methods) if owner
57
65
  when Prism::DefNode
58
- discover_method(node, scope, singleton, path)
66
+ discover_method(node, scope, singleton, path, methods)
59
67
  when Prism::CallNode
60
- discover_call(node, scope, singleton, path)
68
+ discover_call(node, scope, singleton, path, methods)
61
69
  else
62
- node.compact_child_nodes.flat_map { |child| discover(child, scope, singleton, path) }
70
+ node.compact_child_nodes.each { |child| discover(child, scope, singleton, path, methods) }
63
71
  end
72
+ methods
64
73
  end
65
74
 
66
- def discover_class(node, scope, singleton, path)
67
- # @type var methods: Array[MethodInfo]
68
- methods = node.is_a?(Prism::ClassNode) ? discover(node.superclass, scope, singleton, path) : []
75
+ def discover_class(node, scope, singleton, path, methods)
76
+ discover(node.superclass, scope, singleton, path, methods) if node.is_a?(Prism::ClassNode)
69
77
  owner = qualify(node.constant_path, scope, declaration: true)
70
- methods + (owner ? discover(node.body, owner, false, path) : [])
78
+ discover(node.body, owner, false, path, methods) if owner
71
79
  end
72
80
 
73
- def discover_call(node, scope, singleton, path)
81
+ def discover_call(node, scope, singleton, path, methods)
74
82
  name = defined_name(node)
75
83
  block = node.block
76
84
  if name && block.is_a?(Prism::BlockNode)
77
- discover_block(block, name, scope, singleton, path)
85
+ discover_block(block, name, scope, singleton, path, methods)
78
86
  else
79
- node.compact_child_nodes.flat_map { |child| discover(child, scope, singleton, path) }
87
+ node.compact_child_nodes.each { |child| discover(child, scope, singleton, path, methods) }
80
88
  end
81
89
  end
82
90
 
83
- def discover_block(block, name, scope, singleton, path)
91
+ def discover_block(block, name, scope, singleton, path, methods)
84
92
  raise EncodingError unless name.valid_encoding?
85
93
 
86
94
  name = name.encode(Encoding::UTF_8)
87
- parameters = block.parameters
88
- header = parameters.is_a?(Prism::BlockParametersNode) ? parameters.location : block.opening_loc
89
- lines = body_lines(block.body, after: header.end_line, ending: block.location)
95
+ if @coverage
96
+ parameters = block.parameters
97
+ header = parameters.is_a?(Prism::BlockParametersNode) ? parameters.location : block.opening_loc
98
+ lines = body_lines(block.body, after: header.end_line, ending: block.location)
99
+ end
90
100
  name = "self.#{name}" if singleton
91
- [MethodInfo.new(name, scope.empty? ? "Object" : scope.join("::"), 1 + decisions(block.body), lines, path)] +
92
- discover(block.parameters, scope, singleton, path) + discover(block.body, scope, singleton, path)
101
+ methods << MethodInfo.new(name, scope.empty? ? "Object" : scope.join("::"),
102
+ 1 + decisions(block.body), lines, path)
103
+ discover(block.parameters, scope, singleton, path, methods)
104
+ discover(block.body, scope, singleton, path, methods)
93
105
  rescue EncodingError
94
106
  raise Error, "#{path}:#{block.location.start_line}: define_method name must be representable in UTF-8"
95
107
  end
@@ -111,18 +123,21 @@ module Crap4Ruby
111
123
  # @param scope [Array<String>] lexical namespace components
112
124
  # @param singleton [Boolean] whether the enclosing scope is a singleton class
113
125
  # @param path [String] source label
114
- # @return [Array<MethodInfo>] this method and its nested definitions
115
- def discover_method(node, scope, singleton, path)
126
+ # @param methods [Array<MethodInfo>] ordered output shared by the traversal
127
+ # @return [void]
128
+ def discover_method(node, scope, singleton, path, methods)
116
129
  owner = method_owner(node, scope)
117
- return [] unless owner
130
+ return unless owner
118
131
 
119
132
  name = "#{"self." if singleton || node.receiver}#{node.name}"
120
- header = node.equal_loc || node.rparen_loc || node.parameters&.location || node.name_loc
121
- lines = body_lines(node.body, after: header.end_line, ending: node.location)
133
+ if @coverage
134
+ header = node.equal_loc || node.rparen_loc || node.parameters&.location || node.name_loc
135
+ lines = body_lines(node.body, after: header.end_line, ending: node.location)
136
+ end
122
137
  complexity = 1 + decisions(node.body)
123
- [MethodInfo.new(name, owner.empty? ? "Object" : owner.join("::"), complexity, lines, path)] +
124
- discover(node.parameters, scope, singleton, path) +
125
- discover(node.body, scope, singleton, path)
138
+ methods << MethodInfo.new(name, owner.empty? ? "Object" : owner.join("::"), complexity, lines, path)
139
+ discover(node.parameters, scope, singleton, path, methods)
140
+ discover(node.body, scope, singleton, path, methods)
126
141
  end
127
142
 
128
143
  def method_owner(node, scope)
@@ -201,10 +216,21 @@ module Crap4Ruby
201
216
  def decision?(node)
202
217
  return !node.begin_modifier? if node.is_a?(Prism::WhileNode) || node.is_a?(Prism::UntilNode)
203
218
 
204
- %i[if_node unless_node for_node
205
- when_node in_node rescue_modifier_node and_node or_node].include?(node.type) ||
206
- node.type.to_s.match?(/_(?:and|or)_write_node\z/) ||
207
- (node.is_a?(Prism::BeginNode) && !node.rescue_clause.nil?)
219
+ case node.type
220
+ when :if_node, :unless_node, :for_node, :when_node, :in_node,
221
+ :rescue_modifier_node, :and_node, :or_node,
222
+ :call_and_write_node, :call_or_write_node,
223
+ :class_variable_and_write_node, :class_variable_or_write_node,
224
+ :constant_and_write_node, :constant_or_write_node,
225
+ :constant_path_and_write_node, :constant_path_or_write_node,
226
+ :global_variable_and_write_node, :global_variable_or_write_node,
227
+ :index_and_write_node, :index_or_write_node,
228
+ :instance_variable_and_write_node, :instance_variable_or_write_node,
229
+ :local_variable_and_write_node, :local_variable_or_write_node
230
+ true
231
+ else
232
+ node.is_a?(Prism::BeginNode) && !node.rescue_clause.nil?
233
+ end
208
234
  end
209
235
 
210
236
  def safe_navigation?(node, seen)
@@ -225,7 +251,7 @@ module Crap4Ruby
225
251
  end
226
252
 
227
253
  def iteration?(node)
228
- return false unless node.is_a?(Prism::CallNode) && ITERATING_METHODS.include?(node.name)
254
+ return false unless node.is_a?(Prism::CallNode) && ITERATING_METHODS.key?(node.name)
229
255
 
230
256
  block = node.block
231
257
  return false unless block
@@ -236,10 +262,12 @@ module Crap4Ruby
236
262
  end
237
263
 
238
264
  def boundary?(node)
239
- [Prism::DefNode, Prism::ClassNode, Prism::ModuleNode, Prism::SingletonClassNode].any? do |type|
240
- node.is_a?(type)
241
- end ||
265
+ case node
266
+ when Prism::DefNode, Prism::ClassNode, Prism::ModuleNode, Prism::SingletonClassNode
267
+ true
268
+ else
242
269
  !defined_name(node).nil?
270
+ end
243
271
  end
244
272
 
245
273
  # Selects body lines without nested methods, classes, or modules.
@@ -257,7 +285,7 @@ module Crap4Ruby
257
285
  return nil if start.location.start_line <= after
258
286
 
259
287
  excluded = [] #: Array[Integer]
260
- last_line = body_end_line(node, excluded)
288
+ last_line = body_end_node(node, excluded).location.end_line
261
289
  lines = (start.location.start_line..last_line).to_a - excluded
262
290
  return nil if lines.include?(ending.end_line) && trailing_code?(ending)
263
291
 
@@ -270,14 +298,15 @@ module Crap4Ruby
270
298
  end
271
299
 
272
300
  # Heredoc interpolations can extend beyond their parent node's location.
273
- def body_end_line(node, excluded)
274
- last_line = node.location.end_line
301
+ # Compare byte offsets to defer line resolution until a range is needed.
302
+ def body_end_node(node, excluded)
303
+ last = node
275
304
  node.compact_child_nodes.each do |child|
276
- child_line = body_end_line(child, excluded)
277
- last_line = child_line if child_line > last_line
305
+ child_end = body_end_node(child, excluded)
306
+ last = child_end if child_end.end_offset > last.end_offset
278
307
  end
279
- excluded.concat((node.location.start_line..last_line).to_a) if boundary?(node)
280
- last_line
308
+ excluded.concat((node.location.start_line..last.location.end_line).to_a) if boundary?(node)
309
+ last
281
310
  end
282
311
  end
283
312
  end
@@ -0,0 +1,172 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "etc"
4
+
5
+ module Crap4Ruby
6
+ # Analyzes selected files and scores their methods in input order.
7
+ module Batch
8
+ # Reads Ruby source without executing it and preserves method order per file.
9
+ #
10
+ # @param files [Array<String>] source paths in analysis order
11
+ # @param coverage [Coverage, nil] measured coverage, or nil for complexity only
12
+ # @param jobs [Integer, nil] worker count, or nil for automatic selection
13
+ # @return [Array<Entry>] scored methods in file and traversal order
14
+ # @raise [ArgumentError] if the requested worker count is not positive
15
+ def self.call(files, coverage, jobs: nil)
16
+ raise ArgumentError, "jobs must be positive" if jobs && jobs < 1
17
+
18
+ count = worker_count(files.size, jobs)
19
+ count > 1 ? parallel(files, coverage, count) : serial(files, coverage)
20
+ end
21
+
22
+ class << self
23
+ private
24
+
25
+ def worker_count(size, jobs)
26
+ return 1 if size < 2 || jobs == 1 || (!jobs && size < 64)
27
+ return 1 unless Process.respond_to?(:fork) && Thread.list.one?
28
+
29
+ [size, jobs || [4, Etc.nprocessors].min].min
30
+ end
31
+
32
+ def serial(files, coverage)
33
+ analyzer = Analyzer.new(coverage: !coverage.nil?)
34
+ files.flat_map do |path|
35
+ analyzer.call(File.binread(path), path: path).map { |method| Score.call(method, coverage) }
36
+ end
37
+ end
38
+
39
+ def parallel(files, coverage, count)
40
+ # @type var workers: Array[worker]
41
+ workers = [] #: Array[worker]
42
+ begin
43
+ files.each_slice((files.size + count - 1) / count) do |chunk|
44
+ Thread.handle_interrupt(Exception => :never) { start_worker(chunk, coverage, workers) }
45
+ end
46
+ rescue NotImplementedError, Errno::EAGAIN, Errno::ENOMEM, Errno::EMFILE, Errno::ENFILE
47
+ cleanup(workers)
48
+ return serial(files, coverage)
49
+ end
50
+ results = workers.map { |worker| receive(worker) }
51
+ results.flat_map do |result|
52
+ raise result if result.is_a?(StandardError)
53
+
54
+ result
55
+ end
56
+ ensure
57
+ cleanup(workers)
58
+ end
59
+
60
+ def start_worker(files, coverage, workers)
61
+ # @type var pipes: Array[IO]
62
+ pipes = []
63
+ pid = nil #: Integer?
64
+ begin
65
+ 2.times { pipes.concat(IO.pipe) }
66
+ pid = Process.fork do
67
+ run_child(pipes, files, coverage, workers)
68
+ end
69
+ workers << [pipes.fetch(0), pid, pipes.fetch(3)]
70
+ ensure
71
+ close_pipe(pipes[1])
72
+ close_pipe(pipes[2])
73
+ unless pid
74
+ close_pipe(pipes[0])
75
+ close_pipe(pipes[3])
76
+ end
77
+ end
78
+ end
79
+
80
+ def run_child(pipes, files, coverage, workers)
81
+ # @type var status: Integer
82
+ status = 1
83
+ workers.each do |worker|
84
+ close_pipe(worker[0])
85
+ close_pipe(worker[2])
86
+ end
87
+ close_pipe(pipes.fetch(0))
88
+ close_pipe(pipes.fetch(3))
89
+ cancellation = pipes.fetch(2)
90
+ Thread.new do
91
+ cancellation.read
92
+ Process.exit!(1)
93
+ end
94
+ result = child_result(files, coverage)
95
+ Marshal.dump(result, pipes.fetch(1))
96
+ status = 0
97
+ ensure
98
+ begin
99
+ close_pipe(pipes[1])
100
+ ensure
101
+ Process.exit!(status)
102
+ end
103
+ end
104
+
105
+ def child_result(files, coverage)
106
+ serial(files, coverage)
107
+ rescue StandardError => error
108
+ error
109
+ end
110
+
111
+ def receive(worker)
112
+ # @type var reader: IO
113
+ reader = worker[0]
114
+ data = reader.read
115
+ status = reap(worker)
116
+ if status && !status.success?
117
+ raise Error,
118
+ "analysis worker failed (exit #{status.exitstatus || "signal #{status.termsig}"})"
119
+ end
120
+
121
+ payload = decode(data)
122
+ return payload if payload.is_a?(StandardError)
123
+ return payload if payload.is_a?(Array) && payload.all?(Entry)
124
+
125
+ raise Error, "analysis worker returned invalid results"
126
+ rescue EOFError, TypeError, ArgumentError => error
127
+ raise Error, "analysis worker returned an invalid payload: #{error.message}"
128
+ ensure
129
+ reader.close unless reader.closed?
130
+ end
131
+
132
+ def decode(data)
133
+ # Only this call's child processes supply Marshal data; receive validates its shape.
134
+ # rubocop:disable-next Security/MarshalLoad
135
+ Marshal.load(data)
136
+ end
137
+
138
+ def cleanup(workers)
139
+ Thread.handle_interrupt(Exception => :never) do
140
+ workers.each do |worker|
141
+ close_pipe(worker[2])
142
+ close_pipe(worker[0])
143
+ end
144
+ # Close every cancellation channel before waiting for any worker.
145
+ # rubocop:disable-next Style/CombinableLoops
146
+ workers.each { |worker| reap(worker) }
147
+ end
148
+ end
149
+
150
+ def reap(worker)
151
+ pid = worker[1]
152
+ return unless pid
153
+
154
+ Thread.handle_interrupt(Exception => :never) do
155
+ # @type var status: Process::Status?
156
+ begin
157
+ _, status = Process.wait2(pid)
158
+ rescue Errno::ECHILD
159
+ # A host's SIGCHLD handler may have consumed the exit status.
160
+ status = nil
161
+ end
162
+ worker[1] = nil
163
+ status
164
+ end
165
+ end
166
+
167
+ def close_pipe(pipe)
168
+ pipe.close if pipe && !pipe.closed?
169
+ end
170
+ end
171
+ end
172
+ end
data/lib/crap4ruby/cli.rb CHANGED
@@ -33,7 +33,7 @@ module Crap4Ruby
33
33
 
34
34
  files = Discovery.call(root: root, source_roots: options[:source_roots], paths: paths, changed: options[:changed])
35
35
  coverage = load_coverage(options, root) unless options[:no_coverage] || files.empty?
36
- entries = Report.sorted(analyze(files, coverage))
36
+ entries = Report.sorted(Batch.call(files, coverage, jobs: options[:jobs]))
37
37
  unless options[:no_save]
38
38
  path = Report.write(entries, root: root)
39
39
  @stderr.puts("Wrote #{path}")
@@ -47,12 +47,6 @@ module Crap4Ruby
47
47
 
48
48
  private
49
49
 
50
- def analyze(files, coverage)
51
- files.flat_map do |path|
52
- Analyzer.new.call(File.binread(path), path: path).map { |method| Score.call(method, coverage) }
53
- end
54
- end
55
-
56
50
  # Removes parsed options from arguments and returns the remaining path selectors.
57
51
  #
58
52
  # @param arguments [Array<String>] mutable command-line tokens
@@ -78,6 +72,12 @@ module Crap4Ruby
78
72
  end
79
73
  cli.on("--no-coverage", "Complexity only; coverage and CRAP are N/A") { options[:no_coverage] = true }
80
74
  cli.on("--no-save", "Print results without writing .metrics/crap.edn") { options[:no_save] = true }
75
+ cli.on("--jobs N", Integer, "Worker processes (default: automatic; 1 disables workers)") do |jobs|
76
+ # @type var jobs: Integer
77
+ raise OptionParser::InvalidArgument, "jobs must be positive" if jobs < 1
78
+
79
+ options[:jobs] = jobs
80
+ end
81
81
  cli.on("--use-existing-coverage", "Read reports without running tests (the default)") do
82
82
  options[:existing] = true
83
83
  end
@@ -79,7 +79,10 @@ module Crap4Ruby
79
79
 
80
80
  # Every element was validated above; Steep cannot narrow an array through all?.
81
81
  lines = lines #: Array[Integer?]
82
- previous = @files[key] || []
82
+ # @type var previous: Array[Integer?]?
83
+ previous = @files[key]
84
+ return @files[key] = lines.dup unless previous
85
+
83
86
  @files[key] = Array.new([previous.size, lines.size].max) do |index|
84
87
  [previous[index], lines[index]].compact.max
85
88
  end
@@ -66,8 +66,12 @@ module Crap4Ruby
66
66
  name = File.basename(path)
67
67
  if File.directory?(path)
68
68
  Find.prune if path != root && (name.start_with?(".") || EXCLUDED.include?(name) || File.symlink?(path))
69
- elsif !File.symlink?(path) && !name.start_with?(".") && !name.match?(/\A(?:test|spec)_|_(?:test|spec)\.rb\z/)
70
- paths << path
69
+ else
70
+ unless File.symlink?(path) || name.start_with?(".") || name.match?(/\A(?:test|spec)_|_(?:test|spec)\.rb\z/)
71
+ paths << path
72
+ end
73
+ # The file type is known; skip Find's repeated lstat.
74
+ Find.prune
71
75
  end
72
76
  end
73
77
  paths
@@ -26,11 +26,8 @@ module Crap4Ruby
26
26
  entry.crap ? format("%.1f", entry.crap) : "N/A"]
27
27
  end
28
28
  widths = rows.transpose.map { |column| column.map(&:length).max }
29
- lines = rows.map do |row|
30
- row.each_with_index.map do |value, index|
31
- index < 2 ? value.ljust(widths[index]) : value.rjust(widths[index])
32
- end.join(" ")
33
- end
29
+ template = "%-#{widths[0]}s %-#{widths[1]}s %#{widths[2]}s %#{widths[3]}s %#{widths[4]}s"
30
+ lines = rows.map { |row| format(template, *row) }
34
31
  lines.insert(1, "-" * lines.first.length)
35
32
  "#{(["CRAP Report", "==========="] + lines).join("\n")}\n"
36
33
  end
@@ -63,7 +60,8 @@ module Crap4Ruby
63
60
  def self.number(value)
64
61
  return "nil" if value.nil?
65
62
 
66
- text = format("%.4f", value).sub(/0+\z/, "")
63
+ text = format("%.4f", value)
64
+ text.sub!(/0+\z/, "")
67
65
  text.end_with?(".") ? "#{text}0" : text
68
66
  end
69
67
 
@@ -12,8 +12,12 @@ module Crap4Ruby
12
12
  def self.call(method, coverage)
13
13
  fraction = coverage&.fraction(method)
14
14
  if fraction
15
- uncovered = 1 - fraction
16
- crap = (method.complexity * method.complexity * uncovered * uncovered * uncovered) + method.complexity
15
+ # Combine integer terms before constructing the exact score.
16
+ denominator = fraction.denominator
17
+ uncovered = denominator - fraction.numerator
18
+ cube = denominator * denominator * denominator
19
+ complexity = method.complexity
20
+ crap = Rational((complexity * complexity * uncovered * uncovered * uncovered) + (complexity * cube), cube)
17
21
  end
18
22
  Entry.new(method.name, method.namespace, method.complexity, fraction && (fraction * 100).to_f, crap, method.path)
19
23
  end
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Crap4Ruby
4
4
  # Gem version used by the CLI and release process.
5
- VERSION = "0.2.1"
5
+ VERSION = "0.2.2"
6
6
  end
data/lib/crap4ruby.rb CHANGED
@@ -26,7 +26,7 @@ module Crap4Ruby
26
26
  # attr_reader :namespace
27
27
  # # @return [Integer] cyclomatic complexity of the body
28
28
  # attr_reader :complexity
29
- # # @return [Array<Integer>, nil] one-based body lines, or nil when line ownership is ambiguous
29
+ # # @return [Array<Integer>, nil] one-based body lines, or nil when disabled or ambiguous
30
30
  # attr_reader :lines
31
31
  # # @return [String] source path supplied to the analyzer
32
32
  # attr_reader :path
@@ -50,6 +50,7 @@ end
50
50
  require_relative "crap4ruby/analyzer"
51
51
  require_relative "crap4ruby/coverage"
52
52
  require_relative "crap4ruby/score"
53
+ require_relative "crap4ruby/batch"
53
54
  require_relative "crap4ruby/report"
54
55
  require_relative "crap4ruby/discovery"
55
56
  require_relative "crap4ruby/cli"
data/sig/crap4ruby.rbs CHANGED
@@ -31,14 +31,16 @@ module Crap4Ruby
31
31
  end
32
32
 
33
33
  class Analyzer
34
- ITERATING_METHODS: Array[Symbol]
34
+ @coverage: bool
35
+ ITERATING_METHODS: Hash[Symbol, bool]
36
+ def initialize: (?coverage: bool) -> void
35
37
  def call: (String source, path: String) -> Array[MethodInfo]
36
38
  private
37
- def discover: (Prism::Node? node, Array[String] scope, bool singleton, String path) -> Array[MethodInfo]
38
- def discover_class: (Prism::ClassNode | Prism::ModuleNode node, Array[String] scope, bool singleton, String path) -> Array[MethodInfo]
39
- def discover_call: (Prism::CallNode node, Array[String] scope, bool singleton, String path) -> Array[MethodInfo]
40
- def discover_method: (Prism::DefNode node, Array[String] scope, bool singleton, String path) -> Array[MethodInfo]
41
- def discover_block: (Prism::BlockNode block, String name, Array[String] scope, bool singleton, String path) -> Array[MethodInfo]
39
+ def discover: (Prism::Node? node, Array[String] scope, bool singleton, String path, ?Array[MethodInfo] methods) -> Array[MethodInfo]
40
+ def discover_class: (Prism::ClassNode | Prism::ModuleNode node, Array[String] scope, bool singleton, String path, Array[MethodInfo] methods) -> void
41
+ def discover_call: (Prism::CallNode node, Array[String] scope, bool singleton, String path, Array[MethodInfo] methods) -> void
42
+ def discover_method: (Prism::DefNode node, Array[String] scope, bool singleton, String path, Array[MethodInfo] methods) -> void
43
+ def discover_block: (Prism::BlockNode block, String name, Array[String] scope, bool singleton, String path, Array[MethodInfo] methods) -> void
42
44
  def defined_name: (Prism::Node node) -> String?
43
45
  def method_owner: (Prism::DefNode node, Array[String] scope) -> Array[String]?
44
46
  def qualify: (Prism::Node? node, Array[String] scope, ?declaration: bool) -> Array[String]?
@@ -51,7 +53,7 @@ module Crap4Ruby
51
53
  def boundary?: (Prism::Node node) -> bool
52
54
  def body_lines: (Prism::Node? node, after: Integer, ending: Prism::Location) -> Array[Integer]?
53
55
  def trailing_code?: (Prism::Location location) -> bool
54
- def body_end_line: (Prism::Node node, Array[Integer] excluded) -> Integer
56
+ def body_end_node: (Prism::Node node, Array[Integer] excluded) -> Prism::Node
55
57
  end
56
58
 
57
59
  type json = nil | bool | Integer | Float | String | Array[json] | Hash[String, json]
@@ -79,6 +81,22 @@ module Crap4Ruby
79
81
  def self.call: (MethodInfo method, Coverage? coverage) -> Entry
80
82
  end
81
83
 
84
+ module Batch
85
+ type worker = [IO, Integer?, IO]
86
+ def self.call: (Array[String] files, Coverage? coverage, ?jobs: Integer?) -> Array[Entry]
87
+ def self.worker_count: (Integer size, Integer? jobs) -> Integer
88
+ def self.serial: (Array[String] files, Coverage? coverage) -> Array[Entry]
89
+ def self.parallel: (Array[String] files, Coverage? coverage, Integer count) -> Array[Entry]
90
+ def self.start_worker: (Array[String] files, Coverage? coverage, Array[worker] workers) -> void
91
+ def self.run_child: (Array[IO] pipes, Array[String] files, Coverage? coverage, Array[worker] workers) -> void
92
+ def self.child_result: (Array[String] files, Coverage? coverage) -> (Array[Entry] | StandardError)
93
+ def self.receive: (worker worker) -> (Array[Entry] | StandardError)
94
+ def self.decode: (String data) -> Object
95
+ def self.cleanup: (Array[worker] workers) -> void
96
+ def self.reap: (worker worker) -> Process::Status?
97
+ def self.close_pipe: (IO? pipe) -> void
98
+ end
99
+
82
100
  module Report
83
101
  def self.sorted: (Array[Entry] entries) -> Array[Entry]
84
102
  def self.table: (Array[Entry] entries) -> String
@@ -95,13 +113,13 @@ module Crap4Ruby
95
113
 
96
114
  class CLI
97
115
  type options = { root: String, source_roots: Array[String], changed: bool, coverage: String,
98
- ?help: bool, ?version: bool, ?no_coverage: bool, ?no_save: bool, ?existing: bool, ?command: String, ?threshold: Rational }
116
+ ?help: bool, ?version: bool, ?no_coverage: bool, ?no_save: bool, ?existing: bool, ?command: String,
117
+ ?threshold: Rational, ?jobs: Integer }
99
118
  @stdout: _Output
100
119
  @stderr: _Output
101
120
  def initialize: (?stdout: _Output, ?stderr: _Output) -> void
102
121
  def run: (Array[String] arguments) -> Integer
103
122
  private
104
- def analyze: (Array[String] files, Coverage? coverage) -> Array[Entry]
105
123
  def parse_options: (Array[String] arguments) -> [options, OptionParser, Array[String]]
106
124
  def validate_options: (options options) -> void
107
125
  def threshold_status: (Array[Entry] entries, Rational? threshold) -> Integer
data/sig/process.rbs ADDED
@@ -0,0 +1,12 @@
1
+ # Fork-related APIs missing or incomplete in the bundled core signatures.
2
+ module Process
3
+ def self.fork: () { () -> void } -> Integer
4
+ def self.exit!: (?Integer status) -> bot
5
+ end
6
+
7
+ class Thread
8
+ def self.list: () -> Array[Thread]
9
+ | ...
10
+ def self.handle_interrupt: [T] (Hash[Class, Symbol] mask) { () -> T } -> T
11
+ | ...
12
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: crap4ruby
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
  - Leonid Svyatov
@@ -40,12 +40,14 @@ files:
40
40
  - CODE_OF_CONDUCT.md
41
41
  - CONTRIBUTING.md
42
42
  - LICENSE
43
+ - PERFORMANCE.md
43
44
  - README.md
44
45
  - SECURITY.md
45
46
  - VERSIONING.md
46
47
  - exe/crap4ruby
47
48
  - lib/crap4ruby.rb
48
49
  - lib/crap4ruby/analyzer.rb
50
+ - lib/crap4ruby/batch.rb
49
51
  - lib/crap4ruby/cli.rb
50
52
  - lib/crap4ruby/coverage.rb
51
53
  - lib/crap4ruby/discovery.rb
@@ -57,6 +59,7 @@ files:
57
59
  - scoring-audit.md
58
60
  - sig/crap4ruby.rbs
59
61
  - sig/manifest.yaml
62
+ - sig/process.rbs
60
63
  - sig/stdlib.rbs
61
64
  homepage: https://github.com/svyatov/crap4ruby
62
65
  licenses: