rspec-let-analyzer 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.
@@ -0,0 +1,125 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'prism'
4
+
5
+ module RSpecLetAnalyzer
6
+ module Visitors
7
+ class PrismVisitor < Prism::Visitor
8
+ attr_reader :root_lets, :total_its, :redefinitions, :nesting_counts, :factory_usage, :before_creates
9
+
10
+ def initialize(max_nesting_depth, track_factories)
11
+ @root_lets = 0
12
+ @total_its = 0
13
+ @redefinitions = 0
14
+ @depth = 0
15
+ @let_names_by_depth = {}
16
+ @max_nesting_depth = max_nesting_depth
17
+ @nesting_counts = Array.new(max_nesting_depth, 0) if max_nesting_depth
18
+ @track_factories = track_factories
19
+ @factory_usage = Hash.new(0) if track_factories
20
+ @before_creates = 0
21
+ @in_before_block = false
22
+ end
23
+
24
+ def visit_call_node(node)
25
+ method_name = node.name
26
+
27
+ case method_name
28
+ when :describe, :context, :shared_examples, :shared_context
29
+ @depth += 1
30
+ super
31
+ @depth -= 1
32
+ # Clean up let names at this depth when exiting
33
+ @let_names_by_depth.delete(@depth + 1)
34
+ when :before
35
+ # Check if this is before or before(:each)
36
+ arg = node.arguments&.arguments&.first
37
+ is_before_each = arg.nil? || (arg.is_a?(Prism::SymbolNode) && arg.unescaped == 'each')
38
+
39
+ if is_before_each
40
+ @in_before_block = true
41
+ super
42
+ @in_before_block = false
43
+ else
44
+ super
45
+ end
46
+ when :let, :let!
47
+ # Get the let variable name
48
+ let_name = extract_let_name(node)
49
+
50
+ if let_name
51
+ # Check if this let redefines one from an outer scope
52
+ @redefinitions += 1 if redefinition?(let_name)
53
+
54
+ # Track this let name at current depth
55
+ @let_names_by_depth[@depth] ||= []
56
+ @let_names_by_depth[@depth] << let_name
57
+ end
58
+
59
+ # Count by nesting depth
60
+ if @depth == 1
61
+ @root_lets += 1
62
+ elsif @max_nesting_depth
63
+ # Track nesting counts: depth 2 -> index 0, depth 3 -> index 1, etc.
64
+ nesting_index = @depth - 2
65
+ if nesting_index < @max_nesting_depth - 1
66
+ @nesting_counts[nesting_index] += 1
67
+ else
68
+ # Everything at max depth or beyond goes into the last bucket
69
+ @nesting_counts[@max_nesting_depth - 1] += 1
70
+ end
71
+ end
72
+ super
73
+ when :it, :specify
74
+ @total_its += 1
75
+ super
76
+ when :create, :build, :build_stubbed
77
+ # Track if create is inside a before block
78
+ if @in_before_block && method_name == :create
79
+ @before_creates += 1
80
+ end
81
+
82
+ # Track FactoryBot usage
83
+ if @track_factories
84
+ factory_name = extract_factory_name(node)
85
+ @factory_usage[factory_name] += 1 if factory_name
86
+ end
87
+ super
88
+ else
89
+ super
90
+ end
91
+ end
92
+
93
+ private
94
+
95
+ def extract_let_name(node)
96
+ # The first argument to let() is typically a symbol representing the variable name
97
+ return nil unless node.arguments&.arguments&.any?
98
+
99
+ first_arg = node.arguments.arguments.first
100
+ case first_arg
101
+ when Prism::SymbolNode
102
+ first_arg.unescaped
103
+ end
104
+ end
105
+
106
+ def extract_factory_name(node)
107
+ # Handle both create(:factory) and FactoryBot.create(:factory)
108
+ return nil unless node.arguments&.arguments&.any?
109
+
110
+ first_arg = node.arguments.arguments.first
111
+ case first_arg
112
+ when Prism::SymbolNode
113
+ first_arg.unescaped.to_sym
114
+ end
115
+ end
116
+
117
+ def redefinition?(let_name)
118
+ # Check all outer scopes (lower depths) for this let name
119
+ (1...@depth).any? do |outer_depth|
120
+ @let_names_by_depth[outer_depth]&.include?(let_name)
121
+ end
122
+ end
123
+ end
124
+ end
125
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'prism'
4
+ require_relative 'rspec_let_analyzer/version'
5
+ require_relative 'rspec_let_analyzer/adapter'
6
+ require_relative 'rspec_let_analyzer/visitors/prism_visitor'
7
+ require_relative 'rspec_let_analyzer/visitors/parser_visitor'
8
+ require_relative 'rspec_let_analyzer/analyzer'
9
+ require_relative 'rspec_let_analyzer/progress_reporter'
10
+ require_relative 'rspec_let_analyzer/formatters/ascii_formatter'
11
+ require_relative 'rspec_let_analyzer/formatters/json_formatter'
12
+ require_relative 'rspec_let_analyzer/formatters/html_formatter'
13
+ require_relative 'rspec_let_analyzer/formatters/llm_formatter'
14
+
15
+ module RSpecLetAnalyzer
16
+ end
metadata ADDED
@@ -0,0 +1,103 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rspec-let-analyzer
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - James Cook
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 2025-10-08 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: prism
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '0.19'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '0.19'
26
+ - !ruby/object:Gem::Dependency
27
+ name: rake
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '13.0'
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '13.0'
40
+ - !ruby/object:Gem::Dependency
41
+ name: rspec
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '3.12'
47
+ type: :development
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '3.12'
54
+ description: A tool that parses RSpec test files to identify let/let! declarations
55
+ that could be refactored to use test-prof's let_it_be helper for improved test performance
56
+ email:
57
+ - ''
58
+ executables:
59
+ - rspec-let-analyzer
60
+ extensions: []
61
+ extra_rdoc_files: []
62
+ files:
63
+ - README.md
64
+ - bin/rspec-let-analyzer
65
+ - lib/rspec_let_analyzer.rb
66
+ - lib/rspec_let_analyzer/adapter.rb
67
+ - lib/rspec_let_analyzer/adapters/parser_adapter.rb
68
+ - lib/rspec_let_analyzer/adapters/prism_adapter.rb
69
+ - lib/rspec_let_analyzer/analyzer.rb
70
+ - lib/rspec_let_analyzer/formatters/ascii_formatter.rb
71
+ - lib/rspec_let_analyzer/formatters/html_formatter.rb
72
+ - lib/rspec_let_analyzer/formatters/json_formatter.rb
73
+ - lib/rspec_let_analyzer/formatters/llm_formatter.rb
74
+ - lib/rspec_let_analyzer/progress_reporter.rb
75
+ - lib/rspec_let_analyzer/version.rb
76
+ - lib/rspec_let_analyzer/visitors/parser_visitor.rb
77
+ - lib/rspec_let_analyzer/visitors/prism_visitor.rb
78
+ homepage: https://github.com/jamescook/rspec-let-analyzer
79
+ licenses:
80
+ - MIT
81
+ metadata:
82
+ homepage_uri: https://github.com/jamescook/rspec-let-analyzer
83
+ source_code_uri: https://github.com/jamescook/rspec-let-analyzer
84
+ rubygems_mfa_required: 'true'
85
+ rdoc_options: []
86
+ require_paths:
87
+ - lib
88
+ required_ruby_version: !ruby/object:Gem::Requirement
89
+ requirements:
90
+ - - ">="
91
+ - !ruby/object:Gem::Version
92
+ version: 2.7.0
93
+ required_rubygems_version: !ruby/object:Gem::Requirement
94
+ requirements:
95
+ - - ">="
96
+ - !ruby/object:Gem::Version
97
+ version: '0'
98
+ requirements: []
99
+ rubygems_version: 3.6.2
100
+ specification_version: 4
101
+ summary: Analyze RSpec let declarations to identify refactoring opportunities for
102
+ let_it_be
103
+ test_files: []