hone 0.1.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 (105) hide show
  1. checksums.yaml +7 -0
  2. data/.standard.yml +8 -0
  3. data/CHANGELOG.md +5 -0
  4. data/LICENSE.txt +21 -0
  5. data/README.md +201 -0
  6. data/Rakefile +10 -0
  7. data/examples/.hone/harness.rb +41 -0
  8. data/examples/README.md +22 -0
  9. data/examples/allocation_patterns.rb +66 -0
  10. data/examples/cpu_patterns.rb +50 -0
  11. data/examples/jit_patterns.rb +69 -0
  12. data/exe/hone +7 -0
  13. data/lib/hone/adapters/base.rb +35 -0
  14. data/lib/hone/adapters/fasterer.rb +38 -0
  15. data/lib/hone/adapters/rubocop_performance.rb +85 -0
  16. data/lib/hone/analyzer.rb +258 -0
  17. data/lib/hone/cli.rb +247 -0
  18. data/lib/hone/config.rb +93 -0
  19. data/lib/hone/correlator.rb +250 -0
  20. data/lib/hone/exit_codes.rb +10 -0
  21. data/lib/hone/finding.rb +64 -0
  22. data/lib/hone/finding_filter.rb +57 -0
  23. data/lib/hone/formatters/base.rb +25 -0
  24. data/lib/hone/formatters/filterable.rb +31 -0
  25. data/lib/hone/formatters/github.rb +71 -0
  26. data/lib/hone/formatters/json.rb +75 -0
  27. data/lib/hone/formatters/junit.rb +154 -0
  28. data/lib/hone/formatters/sarif.rb +179 -0
  29. data/lib/hone/formatters/tsv.rb +49 -0
  30. data/lib/hone/harness.rb +57 -0
  31. data/lib/hone/harness_generator.rb +128 -0
  32. data/lib/hone/harness_runner.rb +172 -0
  33. data/lib/hone/method_map.rb +140 -0
  34. data/lib/hone/patterns/README.md +174 -0
  35. data/lib/hone/patterns/array_compact.rb +105 -0
  36. data/lib/hone/patterns/array_include_set.rb +34 -0
  37. data/lib/hone/patterns/base.rb +90 -0
  38. data/lib/hone/patterns/block_to_proc.rb +109 -0
  39. data/lib/hone/patterns/bsearch_vs_find.rb +80 -0
  40. data/lib/hone/patterns/chars_map_ord.rb +42 -0
  41. data/lib/hone/patterns/chars_to_variable.rb +136 -0
  42. data/lib/hone/patterns/chars_to_variable_tainted.rb +136 -0
  43. data/lib/hone/patterns/constant_regexp.rb +74 -0
  44. data/lib/hone/patterns/count_vs_size.rb +35 -0
  45. data/lib/hone/patterns/divmod.rb +92 -0
  46. data/lib/hone/patterns/dynamic_ivar.rb +44 -0
  47. data/lib/hone/patterns/dynamic_ivar_get.rb +33 -0
  48. data/lib/hone/patterns/each_with_index.rb +116 -0
  49. data/lib/hone/patterns/each_with_object.rb +63 -0
  50. data/lib/hone/patterns/flatten_once.rb +28 -0
  51. data/lib/hone/patterns/gsub_to_tr.rb +48 -0
  52. data/lib/hone/patterns/hash_each_key.rb +41 -0
  53. data/lib/hone/patterns/hash_each_value.rb +31 -0
  54. data/lib/hone/patterns/hash_keys_include.rb +30 -0
  55. data/lib/hone/patterns/hash_merge_bang.rb +33 -0
  56. data/lib/hone/patterns/hash_values_include.rb +31 -0
  57. data/lib/hone/patterns/inject_sum.rb +48 -0
  58. data/lib/hone/patterns/kernel_loop.rb +27 -0
  59. data/lib/hone/patterns/lazy_ivar.rb +39 -0
  60. data/lib/hone/patterns/map_compact.rb +32 -0
  61. data/lib/hone/patterns/map_flatten.rb +31 -0
  62. data/lib/hone/patterns/map_select_chain.rb +32 -0
  63. data/lib/hone/patterns/parallel_assignment.rb +127 -0
  64. data/lib/hone/patterns/positive_predicate.rb +27 -0
  65. data/lib/hone/patterns/range_include.rb +34 -0
  66. data/lib/hone/patterns/redundant_string_chars.rb +82 -0
  67. data/lib/hone/patterns/regexp_match.rb +126 -0
  68. data/lib/hone/patterns/reverse_each.rb +30 -0
  69. data/lib/hone/patterns/reverse_first.rb +40 -0
  70. data/lib/hone/patterns/select_count.rb +32 -0
  71. data/lib/hone/patterns/select_first.rb +31 -0
  72. data/lib/hone/patterns/select_map.rb +32 -0
  73. data/lib/hone/patterns/shuffle_first.rb +30 -0
  74. data/lib/hone/patterns/slice_with_length.rb +48 -0
  75. data/lib/hone/patterns/sort_by_first.rb +31 -0
  76. data/lib/hone/patterns/sort_by_last.rb +31 -0
  77. data/lib/hone/patterns/sort_first.rb +52 -0
  78. data/lib/hone/patterns/sort_last.rb +30 -0
  79. data/lib/hone/patterns/sort_reverse.rb +53 -0
  80. data/lib/hone/patterns/string_casecmp.rb +54 -0
  81. data/lib/hone/patterns/string_chars_each.rb +56 -0
  82. data/lib/hone/patterns/string_concat_in_loop.rb +116 -0
  83. data/lib/hone/patterns/string_delete_prefix.rb +53 -0
  84. data/lib/hone/patterns/string_delete_suffix.rb +53 -0
  85. data/lib/hone/patterns/string_empty.rb +64 -0
  86. data/lib/hone/patterns/string_end_with.rb +81 -0
  87. data/lib/hone/patterns/string_shovel.rb +75 -0
  88. data/lib/hone/patterns/string_start_with.rb +80 -0
  89. data/lib/hone/patterns/taint_tracking_base.rb +230 -0
  90. data/lib/hone/patterns/times_map.rb +38 -0
  91. data/lib/hone/patterns/uniq_by.rb +32 -0
  92. data/lib/hone/patterns/yield_vs_block.rb +72 -0
  93. data/lib/hone/profilers/base.rb +162 -0
  94. data/lib/hone/profilers/factory.rb +31 -0
  95. data/lib/hone/profilers/memory_profiler.rb +213 -0
  96. data/lib/hone/profilers/stackprof.rb +99 -0
  97. data/lib/hone/profilers/vernier.rb +147 -0
  98. data/lib/hone/reporter.rb +371 -0
  99. data/lib/hone/scanner.rb +75 -0
  100. data/lib/hone/suggestion_generator.rb +23 -0
  101. data/lib/hone/version.rb +5 -0
  102. data/lib/hone.rb +108 -0
  103. data/logo.png +0 -0
  104. data/sig/hone.rbs +4 -0
  105. metadata +176 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: c35d339dc059e423c261e29acd55d76091c09e94cbd1b945eb5c6436b79909ad
4
+ data.tar.gz: 48ed4d9d8ef4a6484e55fb4fd9d71a0cba5285aee2bc52c6436644c631cec33c
5
+ SHA512:
6
+ metadata.gz: f0cd2110c5a28b7e35cb463e00a118cd63e0a9ef2ca206e4d7bb38afc76223b8c1f319a857f4e9cb788d4a29293694312f874efac0580774d0a0b922afb8e654
7
+ data.tar.gz: 8c2058eed3b4699e9ada63edaa316b2f3029aeb9728568bbb614ba8d521986af7b612be6eb75fc49eda7f6f3c55cc33f7ce94355764a6b73516464c2cf59eb03
data/.standard.yml ADDED
@@ -0,0 +1,8 @@
1
+ # For available configuration options, see:
2
+ # https://github.com/standardrb/standard
3
+ ruby_version: 3.1
4
+ ignore:
5
+ - "test/fixtures/**/*":
6
+ - Style/GlobalVars
7
+ - "test/harness_test.rb":
8
+ - Style/GlobalVars
data/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ ## [Unreleased]
2
+
3
+ ## [0.1.0] - 2025-12-26
4
+
5
+ - Initial release
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2025 Stephen Ierodiaconou
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,201 @@
1
+ <p align="center">
2
+ <img src="logo.png" alt="Hone" width="200">
3
+ </p>
4
+
5
+ # Hone
6
+
7
+ > **Note:** This is currently a proof of concept. APIs and output formats may change.
8
+
9
+ Find Ruby performance optimizations that actually matter by combining static analysis with runtime profiling.
10
+
11
+ ## Example Output
12
+
13
+ When run with profiling data, performance optimisation findings show their potential runtime impact:
14
+
15
+ ```
16
+ Hone v0.1.0
17
+
18
+ Analyzing lib/sqids.rb with profile: tmp/hone/cpu_profile.json
19
+ ────────────────────────────────────────────────────────────────────────
20
+
21
+ lib/sqids.rb:69 in `Sqids#decode`
22
+ 17.5% of allocations — High impact
23
+
24
+ 67 │
25
+ 68 │ alphabet_chars = @alphabet.chars
26
+ 69 │ id.chars.each do |c|
27
+ ^^^^^^^^^^^^^^^^^^^^
28
+ 70 │ return ret unless alphabet_chars.include?(c)
29
+ 71 │ end
30
+
31
+ Use `id.each_char { ... }` instead of `chars.each` to avoid intermediate array
32
+
33
+ ────────────────────────────────────────────────────────────────────────
34
+
35
+ lib/sqids.rb:183 in `Sqids#blocked_id?`
36
+ 13.5% of CPU — High impact
37
+
38
+ 181 │ id.start_with?(word) || id.end_with?(word)
39
+ 182 │ else
40
+ 183 │ id.include?(word)
41
+ ^^^^^^^^^^^^^^^^^
42
+ 184 │ end
43
+ 185 │ end
44
+
45
+ Consider using Set instead of Array#include? for repeated lookups
46
+
47
+ ────────────────────────────────────────────────────────────────────────
48
+
49
+ lib/sqids.rb:104 in `Sqids#shuffle`
50
+ 3.1% of CPU — Moderate
51
+
52
+ 102 │ i = 0
53
+ 103 │ j = chars.length - 1
54
+ 104 │ while j.positive?
55
+ ^^^^^^^^^^^
56
+ 105 │ r = ((i * j) + chars[i].ord + chars[j].ord) % chars.length
57
+ 106 │ chars[i], chars[r] = chars[r], chars[i]
58
+
59
+ Fix: j > 0
60
+ Use `> 0` instead of `.positive?` to avoid method call overhead
61
+
62
+ ────────────────────────────────────────────────────────────────────────
63
+
64
+ Summary: 14 findings
65
+
66
+ 3 high impact (>5% of CPU or allocations) ← fix these first
67
+ 11 moderate (1-5%)
68
+ ```
69
+
70
+ The percentages show how much CPU time or memory allocation each method used during profiling, helping you focus on fixes that matter.
71
+
72
+ ## Installation
73
+
74
+ Add to your Gemfile:
75
+
76
+ ```ruby
77
+ gem 'hone', group: :development
78
+
79
+ # Optional: for profiling integration
80
+ gem 'stackprof', group: :development
81
+ gem 'memory_profiler', group: :development
82
+ ```
83
+
84
+ Or install directly:
85
+
86
+ ```bash
87
+ gem install hone
88
+ ```
89
+
90
+ ## Quick Start
91
+
92
+ ```bash
93
+ # Try it on the included examples
94
+ hone analyze examples/
95
+
96
+ # Or analyze your own code
97
+ hone analyze lib/
98
+ ```
99
+
100
+ ### With Profiling (Recommended)
101
+
102
+ For prioritized results, use a harness:
103
+
104
+ ```bash
105
+ # Create a harness in your project
106
+ hone init harness
107
+
108
+ # Edit .hone/harness.rb to define your workload, then:
109
+ hone profile --analyze
110
+ ```
111
+
112
+ Or provide an existing StackProf profile:
113
+
114
+ ```bash
115
+ hone analyze lib/ --profile stackprof.json
116
+ ```
117
+
118
+ ### About Impact Levels
119
+
120
+ | Level | Meaning |
121
+ |-------|---------|
122
+ | High impact | >5% CPU or allocation - fix these first |
123
+ | JIT issue | Hurts YJIT optimization |
124
+ | Moderate | 1-5% impact - worth fixing |
125
+ | Low | <1% impact - low priority |
126
+
127
+ Without profiling data, findings show "Unknown" impact.
128
+
129
+ ## CLI Reference
130
+
131
+ ```bash
132
+ # Analysis
133
+ hone analyze FILE|DIR # Static analysis
134
+ hone analyze FILE --profile STACKPROF # With CPU correlation
135
+ hone analyze FILE --memory-profile FILE # With memory correlation
136
+
137
+ # Profiling
138
+ hone init harness # Create .hone/harness.rb template
139
+ hone profile # Run harness, generate profiles
140
+ hone profile --analyze # Profile and analyze in one step
141
+ hone profile --memory # Include memory profiling
142
+
143
+ # Output control
144
+ --format FORMAT # text, json, github, sarif, junit, tsv
145
+ --top N # Show only top N findings
146
+ --hot-only # Only high impact findings
147
+ --show-cold # Include low impact findings (hidden by default)
148
+ --quiet / -q # One line per finding
149
+ --verbose / -V # Extended output with pattern details
150
+
151
+ # CI integration
152
+ --fail-on LEVEL # Exit non-zero for: hot, warm, any, none
153
+ --baseline FILE # Suppress known findings
154
+ ```
155
+
156
+ ## Harness
157
+
158
+ The harness defines a repeatable workload for profiling:
159
+
160
+ ```ruby
161
+ # .hone/harness.rb
162
+ setup do
163
+ require_relative '../lib/my_gem'
164
+ @data = load_test_data
165
+ end
166
+
167
+ exercise iterations: 100 do
168
+ MyGem.process(@data)
169
+ end
170
+
171
+ teardown do
172
+ cleanup # optional
173
+ end
174
+ ```
175
+
176
+ - `setup`: Runs once before profiling (load code, prepare data)
177
+ - `exercise`: The code to profile (runs N iterations)
178
+ - `teardown`: Cleanup after profiling (optional)
179
+
180
+ ## Patterns
181
+
182
+ Hone detects 50+ patterns across three categories:
183
+
184
+ - **CPU**: Method call overhead, loop inefficiencies
185
+ - **Allocation**: Intermediate arrays, string allocations
186
+ - **JIT**: Dynamic ivars, shape transitions, YJIT blockers
187
+
188
+ Run `hone patterns` to list all available patterns.
189
+
190
+ ## Requirements
191
+
192
+ - Ruby 3.1+
193
+ - Prism (bundled with Ruby 3.3+, add `gem 'prism'` for earlier versions)
194
+
195
+ ## Acknowledgements
196
+
197
+ Some pattern detections inspired by:
198
+
199
+ - [fast-ruby](https://github.com/fastruby/fast-ruby) - Benchmarks for common Ruby idioms
200
+ - [fasterer](https://github.com/DamirSvrtan/fasterer) - Static analysis for speed improvements
201
+ - [rubocop-performance](https://github.com/rubocop/rubocop-performance) - Performance-focused RuboCop cops
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "minitest/test_task"
5
+
6
+ Minitest::TestTask.create
7
+
8
+ require "standard/rake"
9
+
10
+ task default: %i[test standard]
@@ -0,0 +1,41 @@
1
+ # Hone harness for example patterns
2
+ # Run with: hone profile --analyze
3
+
4
+ setup do
5
+ require_relative "../cpu_patterns"
6
+ require_relative "../allocation_patterns"
7
+ require_relative "../jit_patterns"
8
+
9
+ @cpu = CpuPatterns.new
10
+ @alloc = AllocationPatterns.new
11
+ @jit = JitPatterns.new
12
+ @data = (1..100).to_a
13
+ @str = "hello world" * 10
14
+ end
15
+
16
+ exercise iterations: 1000 do
17
+ # CPU patterns
18
+ @cpu.check_positive(42)
19
+ @cpu.count_up
20
+ @cpu.sum_with_index(@data)
21
+ @cpu.tail(@str)
22
+ @cpu.replace_spaces(@str)
23
+ @cpu.match_pattern(["abc", "123", "def"])
24
+
25
+ # Allocation patterns
26
+ @alloc.char_codes(@str)
27
+ @alloc.transform_and_filter(@data)
28
+ @alloc.iterate_chars("test")
29
+ @alloc.nested_map([1, 2, 3])
30
+ @alloc.total(@data)
31
+ @alloc.random_element(@data)
32
+ @alloc.smallest(@data)
33
+ @alloc.final_element(@data)
34
+ @alloc.build_string(%w[a b c d e])
35
+ @alloc.char_at(@str, 5)
36
+
37
+ # JIT patterns
38
+ @jit.set_field("test", 123)
39
+ @jit.get_field("test")
40
+ @jit.cached_value
41
+ end
@@ -0,0 +1,22 @@
1
+ # Hone Examples
2
+
3
+ Example files with intentional anti-patterns for testing Hone.
4
+
5
+ ## Quick Test
6
+
7
+ ```bash
8
+ cd examples
9
+
10
+ # Static analysis only
11
+ hone analyze .
12
+
13
+ # With profiling (recommended)
14
+ hone profile --memory --analyze
15
+ ```
16
+
17
+ ## Files
18
+
19
+ - `cpu_patterns.rb` - Method call overhead, loop inefficiencies
20
+ - `allocation_patterns.rb` - Intermediate arrays, string allocations
21
+ - `jit_patterns.rb` - YJIT optimization blockers
22
+ - `.hone/harness.rb` - Profiling harness for these examples
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rubocop:disable all
4
+ # standardrb:disable all
5
+
6
+ # Allocation-related patterns that Hone detects
7
+ # NOTE: This file intentionally contains anti-patterns for testing
8
+
9
+ class AllocationPatterns
10
+ # chars.map(&:ord) vs bytes
11
+ def char_codes(str)
12
+ str.chars.map(&:ord) # Hone: Use `str.bytes` instead
13
+ end
14
+
15
+ # map.select vs filter_map
16
+ def transform_and_filter(arr)
17
+ arr.map { |x| x * 2 }.select { |x| x > 10 } # Hone: Use `filter_map`
18
+ end
19
+
20
+ # chars.each vs each_char
21
+ def iterate_chars(str)
22
+ count = 0
23
+ str.chars.each { |c| count += 1 } # Hone: Use `each_char`
24
+ count
25
+ end
26
+
27
+ # map.flatten vs flat_map
28
+ def nested_map(arr)
29
+ arr.map { |x| [x, x * 2] }.flatten # Hone: Use `flat_map`
30
+ end
31
+
32
+ # inject(:+) vs sum
33
+ def total(arr)
34
+ arr.inject(:+) # Hone: Use `sum`
35
+ end
36
+
37
+ # shuffle.first vs sample
38
+ def random_element(arr)
39
+ arr.shuffle.first # Hone: Use `sample`
40
+ end
41
+
42
+ # sort.first vs min
43
+ def smallest(arr)
44
+ arr.sort.first # Hone: Use `min`
45
+ end
46
+
47
+ # reverse.first vs last
48
+ def final_element(arr)
49
+ arr.reverse.first # Hone: Use `last`
50
+ end
51
+
52
+ # String concatenation in loop
53
+ def build_string(parts)
54
+ result = ""
55
+ parts.each do |part|
56
+ result += part # Hone: Use `<<` or array.join
57
+ end
58
+ result
59
+ end
60
+
61
+ # Variable from .chars then indexed
62
+ def char_at(str, i)
63
+ chars = str.chars # Allocates array
64
+ chars[i] # Hone: Use `str[i]` directly
65
+ end
66
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rubocop:disable all
4
+ # standardrb:disable all
5
+
6
+ # CPU-related patterns that Hone detects
7
+ # NOTE: This file intentionally contains anti-patterns for testing
8
+
9
+ class CpuPatterns
10
+ # positive? vs > 0
11
+ def check_positive(n)
12
+ n.positive? # Hone: Use `> 0` instead
13
+ end
14
+
15
+ # loop vs while true
16
+ def count_up
17
+ i = 0
18
+ loop do # Hone: Use `while true` for JIT optimization
19
+ i += 1
20
+ break if i > 100
21
+ end
22
+ i
23
+ end
24
+
25
+ # each_with_index vs manual index
26
+ def sum_with_index(arr)
27
+ total = 0
28
+ arr.each_with_index do |val, i| # Hone: Consider manual indexing in hot loops
29
+ total += val * i
30
+ end
31
+ total
32
+ end
33
+
34
+ # Slice with length vs endless range
35
+ def tail(str)
36
+ str[1, str.length] # Hone: Use `str[1..]` instead
37
+ end
38
+
39
+ # gsub single char vs tr
40
+ def replace_spaces(str)
41
+ str.gsub(" ", "_") # Hone: Use `tr` for single char replacement
42
+ end
43
+
44
+ # Regexp in loop
45
+ def match_pattern(strings)
46
+ strings.select do |s|
47
+ s.match?(/\d+/) # Hone: Extract regexp to constant
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rubocop:disable all
4
+ # standardrb:disable all
5
+
6
+ # JIT optimization patterns that Hone detects
7
+ # NOTE: This file intentionally contains anti-patterns for testing
8
+ # These patterns can prevent YJIT from optimizing effectively
9
+
10
+ class JitPatterns
11
+ # Dynamic instance_variable_set
12
+ def set_field(name, value)
13
+ instance_variable_set("@#{name}", value) # Hone: Causes shape transitions
14
+ end
15
+
16
+ # Dynamic instance_variable_get
17
+ def get_field(name)
18
+ instance_variable_get("@#{name}") # Hone: Consider explicit accessors
19
+ end
20
+
21
+ # Lazy ivar initialization outside initialize
22
+ def cached_value
23
+ @cached ||= expensive_computation # Hone: Initialize in constructor
24
+ end
25
+
26
+ # defined? check for ivar
27
+ def ensure_loaded
28
+ @data = load_data unless defined?(@data) # Hone: Use nil check or initialize
29
+ end
30
+
31
+ private
32
+
33
+ def expensive_computation
34
+ sleep(0.001)
35
+ 42
36
+ end
37
+
38
+ def load_data
39
+ []
40
+ end
41
+ end
42
+
43
+ # RECOMMENDED: Initialize all ivars in constructor for stable object shape.
44
+ # This allows YJIT to optimize method dispatch because the object's
45
+ # memory layout is consistent from creation.
46
+ class StableShape
47
+ def initialize
48
+ @cached = nil
49
+ @data = nil
50
+ end
51
+
52
+ def cached_value
53
+ @cached ||= expensive_computation
54
+ end
55
+
56
+ def data
57
+ @data ||= load_data
58
+ end
59
+
60
+ private
61
+
62
+ def expensive_computation
63
+ 42
64
+ end
65
+
66
+ def load_data
67
+ []
68
+ end
69
+ end
data/exe/hone ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "hone"
5
+ require "hone/cli"
6
+
7
+ Hone::CLI.start(ARGV)
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hone
4
+ module Adapters
5
+ class Base
6
+ def initialize(file_path)
7
+ @file_path = file_path
8
+ end
9
+
10
+ def findings
11
+ raise NotImplementedError
12
+ end
13
+
14
+ private
15
+
16
+ def build_finding(line:, message:, pattern_id:, optimization_type: :cpu, code: nil, speedup: nil)
17
+ Finding.new(
18
+ file: @file_path,
19
+ line: line,
20
+ column: 0,
21
+ pattern_id: pattern_id,
22
+ optimization_type: optimization_type,
23
+ source: source_name,
24
+ message: message,
25
+ speedup: speedup,
26
+ code: code
27
+ )
28
+ end
29
+
30
+ def source_name
31
+ raise NotImplementedError
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "open3"
4
+
5
+ module Hone
6
+ module Adapters
7
+ class Fasterer < Base
8
+ def findings
9
+ return [] unless fasterer_available?
10
+
11
+ parse_output(run_fasterer)
12
+ rescue NotImplementedError => e
13
+ warn "[Hone::Adapters::Fasterer] #{e.message}"
14
+ []
15
+ end
16
+
17
+ private
18
+
19
+ def source_name
20
+ :fasterer
21
+ end
22
+
23
+ def fasterer_available?
24
+ _, _, status = Open3.capture3("which", "fasterer")
25
+ status.success?
26
+ end
27
+
28
+ def run_fasterer
29
+ stdout, _stderr, _status = Open3.capture3("fasterer", @file_path)
30
+ stdout
31
+ end
32
+
33
+ def parse_output(output)
34
+ raise NotImplementedError, "Fasterer output parsing not yet implemented"
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "open3"
4
+ require "json"
5
+
6
+ module Hone
7
+ module Adapters
8
+ class RubocopPerformance < Base
9
+ def findings
10
+ return [] unless rubocop_available? && rubocop_performance_available?
11
+
12
+ parse_output(run_rubocop)
13
+ rescue JSON::ParserError => e
14
+ warn "[Hone::Adapters::RubocopPerformance] Failed to parse RuboCop output: #{e.message}"
15
+ []
16
+ rescue => e
17
+ warn "[Hone::Adapters::RubocopPerformance] #{e.message}"
18
+ []
19
+ end
20
+
21
+ private
22
+
23
+ def source_name
24
+ :rubocop
25
+ end
26
+
27
+ def rubocop_available?
28
+ _, _, status = Open3.capture3("which", "rubocop")
29
+ status.success?
30
+ end
31
+
32
+ def rubocop_performance_available?
33
+ stdout, _, status = Open3.capture3("gem", "list", "rubocop-performance")
34
+ status.success? && stdout.include?("rubocop-performance")
35
+ end
36
+
37
+ def run_rubocop
38
+ stdout, _stderr, _status = Open3.capture3(
39
+ "rubocop",
40
+ "--only", "Performance",
41
+ "--format", "json",
42
+ @file_path
43
+ )
44
+ stdout
45
+ end
46
+
47
+ def parse_output(output)
48
+ return [] if output.nil? || output.strip.empty?
49
+
50
+ data = JSON.parse(output)
51
+ files = data["files"] || []
52
+
53
+ files.flat_map do |file_data|
54
+ offenses = file_data["offenses"] || []
55
+ offenses.map do |offense|
56
+ build_finding(
57
+ line: offense.dig("location", "line") || 0,
58
+ message: offense["message"],
59
+ pattern_id: offense["cop_name"]&.to_sym || :unknown,
60
+ optimization_type: severity_to_optimization_type(offense["severity"]),
61
+ code: extract_code(offense)
62
+ )
63
+ end
64
+ end
65
+ end
66
+
67
+ def severity_to_optimization_type(severity)
68
+ case severity
69
+ when "error", "fatal"
70
+ :cpu
71
+ when "warning"
72
+ :cpu
73
+ when "convention", "refactor"
74
+ :allocation
75
+ else
76
+ :cpu
77
+ end
78
+ end
79
+
80
+ def extract_code(offense)
81
+ offense.dig("location", "source")
82
+ end
83
+ end
84
+ end
85
+ end