ruby-lsp-factory_bot 0.3.0 → 0.3.1

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: e53f81bee237aa1380533073b84434270af2452c98ad6edbb9edbf1663cdbc43
4
- data.tar.gz: 24d5309b51a16ef658f2b15f3f44a5f9bd6fb18743e5f4517344ca130f0aa82d
3
+ metadata.gz: f4ae7bc8179ba4962a90e3adc74721712416deeceb295f410bf67e857c6e7f85
4
+ data.tar.gz: a5186ec32f38cf6013509c4deef814f1c21fe8da584972cca76c59d0c549e654
5
5
  SHA512:
6
- metadata.gz: 27b794a78d67d2ae305f0f099340a183eb92fed59741f4b1264dbffc8f86cdaffbc3efdcd81fa4586b908c71abe260df8f0e3c896197b5478833aa6e466a5426
7
- data.tar.gz: 5f357307d3e982e759a8da970a4aca79886cbb1853c66809c0c00ebb8fd7eaa5cc6da8077038e70e7a0141f1646b9466a22eb8a4d03c0cc22eb11bdb64d27ef4
6
+ metadata.gz: 61d0eff2bd0fdca96578d1440e1182159cd2972a0bce5528e3d14616b984b1d51ac63f841622f88508359f9a99ad29eaeda74b872f2fc57fd790593241005e4b
7
+ data.tar.gz: 56e61ef4a900e2712a8c0e03307c8a19079733ef79382531dddc4bbfe4ac71fbc048ec946c03d4fef0343732daaaa1fe228b21ff15bd6a1c36de2e42c95fe0e2
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/.rubocop.yml ADDED
@@ -0,0 +1,8 @@
1
+ AllCops:
2
+ TargetRubyVersion: 3.0
3
+
4
+ Style/StringLiterals:
5
+ EnforcedStyle: double_quotes
6
+
7
+ Style/StringLiteralsInInterpolation:
8
+ EnforcedStyle: double_quotes
data/README.md ADDED
@@ -0,0 +1,26 @@
1
+ # FactoryBot add-on
2
+
3
+ The FactoryBot add-on is a [Ruby LSP](https://github.com/Shopify/ruby-lsp) [add-on](https://shopify.github.io/ruby-lsp/add-ons.html) for extra FactoryBot editor features.
4
+
5
+ ## Installation
6
+
7
+ If you haven't already done so, you'll need to first [set up Ruby LSP](https://shopify.github.io/ruby-lsp/#usage).
8
+
9
+ Add the following to your application's Gemfile:
10
+ ```ruby
11
+ # Gemfile
12
+ gem "ruby-lsp-factory_bot", require: false, group: :development
13
+ ```
14
+
15
+ run `bundle install` and restart Ruby LSP server.
16
+
17
+ ## Features
18
+
19
+ - [x] Go-to-definition for factories
20
+ - [x] Go-to-definition for factory traits
21
+ - [ ] Completions for factories and traits
22
+
23
+ ## License
24
+
25
+ The gem is available as open source under the terms of the
26
+ [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ require "rubocop/rake_task"
9
+
10
+ RuboCop::RakeTask.new
11
+
12
+ task default: %i[spec rubocop]
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "sorbet-runtime"
4
+ require "ruby_lsp/factory_bot/version"
5
+ require "ruby_lsp/factory_bot/addon"
6
+
7
+ module RubyLsp
8
+ module FactoryBot
9
+ end
10
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ # parts << 'gem "ruby-lsp-factory_bot", require: false, group: :development, path: "~/Projects/ruby-lsp-factory_bot"'
4
+ require "ruby_lsp/addon"
5
+ require "ruby_lsp/internal"
6
+
7
+ require_relative "definition"
8
+ require_relative "indexing_enhancement"
9
+
10
+ module RubyLsp
11
+ module FactoryBot
12
+ class Addon < ::RubyLsp::Addon
13
+ def name
14
+ "Ruby LSP - Factory Bot"
15
+ end
16
+
17
+ def version
18
+ VERSION
19
+ end
20
+
21
+ def activate(global_state, _message_queue)
22
+ @index = global_state.index
23
+ end
24
+
25
+ def deactivate; end
26
+
27
+ def create_definition_listener(response_builder, uri, node_context, dispatcher)
28
+ Definition.new(response_builder, uri, node_context, @index, dispatcher)
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,134 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "utils"
4
+
5
+ module RubyLsp
6
+ module FactoryBot
7
+ class Definition
8
+ include ::RubyLsp::Requests::Support::Common
9
+
10
+ FACTORY_BOT_STRATEGIES = %i[
11
+ create
12
+ build
13
+ build_stubbed
14
+ attributes_for
15
+ ].flat_map { |attr| [attr, :"#{attr}_list", :"#{attr}_pair"] }.freeze
16
+
17
+ def initialize(response_builder, uri, node_context, index, dispatcher)
18
+ @response_builder = response_builder
19
+ @uri = uri
20
+ @node_context = node_context
21
+ @index = index
22
+ dispatcher.register(self, :on_symbol_node_enter, :on_call_node_enter)
23
+ end
24
+
25
+ def on_symbol_node_enter(node)
26
+ return unless (call_node = @node_context&.call_node)
27
+
28
+ case call_node.name
29
+ when *FACTORY_BOT_STRATEGIES then factory_or_trait_location_for(call_node, node)
30
+ when :generate then sequence_location_for(node.value)
31
+ when :factory then target_class_location_for(node)
32
+ end
33
+ end
34
+
35
+ def on_call_node_enter(node)
36
+ return unless @node_context&.call_node&.message == "factory"
37
+ return if node.arguments || node.block
38
+
39
+ factory_location_for(node.message)
40
+ end
41
+
42
+ private
43
+
44
+ def factory_or_trait_location_for(call_node, node)
45
+ called_factory_name = Utils.name_from_node(call_node.arguments&.arguments&.first)
46
+ return unless called_factory_name
47
+
48
+ return factory_location_for(node.value) if node.value == called_factory_name
49
+
50
+ trait_location_for(node.value, called_factory_name)
51
+ end
52
+
53
+ def factory_location_for(factory_name)
54
+ @index["#{factory_name}FactoryBot"]&.each do |entry|
55
+ @response_builder << Interface::LocationLink.new(
56
+ target_uri: URI::Generic.from_path(path: entry.file_path).to_s,
57
+ target_range: range_from_location(entry.location),
58
+ target_selection_range: range_from_location(entry.name_location)
59
+ )
60
+ end
61
+ end
62
+
63
+ def trait_location_for(trait_name, called_factory_name)
64
+ @index["#{called_factory_name}-t-#{trait_name}FactoryBot"]&.each do |entry|
65
+ @response_builder << Interface::LocationLink.new(
66
+ target_uri: URI::Generic.from_path(path: entry.file_path).to_s,
67
+ target_range: range_from_location(entry.location),
68
+ target_selection_range: range_from_location(entry.name_location)
69
+ )
70
+ end
71
+ end
72
+
73
+ def sequence_location_for(sequence_name)
74
+ @index["#{sequence_name}-s-FactoryBot"]&.each do |entry|
75
+ @response_builder << Interface::LocationLink.new(
76
+ target_uri: URI::Generic.from_path(path: entry.file_path).to_s,
77
+ target_range: range_from_location(entry.location),
78
+ target_selection_range: range_from_location(entry.name_location)
79
+ )
80
+ end
81
+ end
82
+
83
+ def target_class_location_for(node)
84
+ call_node = @node_context.call_node
85
+ return unless call_node&.message == "factory"
86
+ return unless inside_factory_bot_define?
87
+
88
+ class_name = resolve_target_class_name_for_factory(call_node, node)
89
+
90
+ @index[class_name]&.each do |entry|
91
+ @response_builder << Interface::LocationLink.new(
92
+ target_uri: URI::Generic.from_path(path: entry.file_path).to_s,
93
+ target_range: range_from_location(entry.location),
94
+ target_selection_range: range_from_location(entry.name_location)
95
+ )
96
+ end
97
+ end
98
+
99
+ def inside_factory_bot_define?
100
+ nesting_nodes = @node_context.instance_variable_get :@nesting_nodes
101
+ body_statements = nesting_nodes.find { |node| node.is_a?(Prism::ProgramNode) }&.statements&.body
102
+
103
+ factory_bot_define = body_statements.find do |statement_body|
104
+ statement_body.is_a?(Prism::CallNode) && statement_body.message == "define"
105
+ end
106
+
107
+ factory_bot_define.block&.body&.body&.any? do |node|
108
+ node.is_a?(Prism::CallNode) && node == @node_context.call_node
109
+ end
110
+ end
111
+
112
+ def resolve_target_class_name_for_factory(call_node, node)
113
+ kwargs_node = call_node&.arguments&.arguments&.find do |argument|
114
+ argument.type == :keyword_hash_node || argument.type == :hash_node
115
+ end
116
+
117
+ class_name_value = kwargs_node&.elements&.find do |element|
118
+ Utils.name_from_node(element.key) == "class_name"
119
+ end&.value
120
+
121
+ case class_name_value
122
+ when Prism::StringNode then return class_name_value.content
123
+ when Prism::SymbolNode then return class_name_value.value.to_s
124
+ when Prism::ConstantReadNode then return class_name_value.name.to_s
125
+ end
126
+
127
+ factory_name = Utils.name_from_node(node)
128
+ return unless factory_name
129
+
130
+ Utils.snake_to_camel(factory_name)
131
+ end
132
+ end
133
+ end
134
+ end
@@ -0,0 +1,114 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "utils"
4
+
5
+ module RubyLsp
6
+ module FactoryBot
7
+ class IndexingEnhancement < RubyIndexer::Enhancement
8
+ def initialize(...)
9
+ super
10
+ @inside_define_block = false
11
+ @factory_stack = []
12
+ end
13
+
14
+ FACTORIES_PATH = "spec/factories"
15
+
16
+ def on_call_node_enter(node)
17
+ @inside_define_block = true if node.message == "define"
18
+ return unless @inside_define_block
19
+
20
+ case node.message
21
+ when "factory"
22
+ @factory_stack << register_factory(node)
23
+ when "trait"
24
+ register_trait(node)
25
+ when "sequence"
26
+ register_sequence(node) if @factory_stack.empty?
27
+ end
28
+ end
29
+
30
+ def on_call_node_leave(node)
31
+ @inside_define_block = false if node.message == "define"
32
+ @factory_stack.pop if node.message == "factory"
33
+ end
34
+
35
+ private
36
+
37
+ def register_factory(node)
38
+ factory_names = resolve_factory_names(node)
39
+ factory_names&.each do |factory_name|
40
+ @listener.add_method(
41
+ "#{factory_name}FactoryBot",
42
+ node.location,
43
+ [RubyIndexer::Entry::Signature.new([])]
44
+ )
45
+ end
46
+
47
+ factory_names
48
+ end
49
+
50
+ def resolve_factory_names(node)
51
+ arguments = node.arguments&.arguments
52
+ return unless arguments
53
+
54
+ factory_names = []
55
+ factory_names << Utils.name_from_node(arguments.first)
56
+
57
+ keyword_hash_node = arguments.find do |argument|
58
+ argument.type == :keyword_hash_node || argument.type == :hash_node
59
+ end
60
+ return factory_names unless keyword_hash_node
61
+
62
+ aliases_node = keyword_hash_node.elements.find do |element|
63
+ Utils.name_from_node(element.key) == "aliases"
64
+ end&.value
65
+ return factory_names unless aliases_node
66
+
67
+ case aliases_node
68
+ when Prism::ArrayNode
69
+ factory_names += aliases_node.elements.map { |element| Utils.name_from_node(element) }
70
+ when Prism::SymbolNode, Prism::StringNode
71
+ factory_names << Utils.name_from_node(aliases_node)
72
+ end
73
+
74
+ factory_names
75
+ end
76
+
77
+ def register_trait(node)
78
+ return if !current_factory_names || current_factory_names.empty?
79
+
80
+ arguments = node.arguments&.arguments
81
+ return unless arguments
82
+
83
+ trait_name = Utils.name_from_node(arguments.first)
84
+ return unless trait_name
85
+
86
+ current_factory_names.each do |factory_name|
87
+ @listener.add_method(
88
+ "#{factory_name}-t-#{trait_name}FactoryBot",
89
+ node.location,
90
+ [RubyIndexer::Entry::Signature.new([])]
91
+ )
92
+ end
93
+ end
94
+
95
+ def current_factory_names
96
+ @factory_stack.last
97
+ end
98
+
99
+ def register_sequence(node)
100
+ arguments = node.arguments&.arguments
101
+ return unless arguments
102
+
103
+ sequence_name = Utils.name_from_node(arguments.first)
104
+ return unless sequence_name
105
+
106
+ @listener.add_method(
107
+ "#{sequence_name}-s-FactoryBot",
108
+ node.location,
109
+ [RubyIndexer::Entry::Signature.new([])]
110
+ )
111
+ end
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Utils
4
+ module_function
5
+
6
+ def name_from_node(name_node)
7
+ case name_node
8
+ when Prism::StringNode
9
+ name_node.content
10
+ when Prism::SymbolNode
11
+ name_node.value
12
+ end
13
+ end
14
+
15
+ def snake_to_camel(snake_str)
16
+ snake_str.split("_").map(&:capitalize).join
17
+ end
18
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLsp
4
+ module FactoryBot
5
+ VERSION = "0.3.1"
6
+ end
7
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/ruby_lsp/factory_bot/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "ruby-lsp-factory_bot"
7
+ spec.version = RubyLsp::FactoryBot::VERSION
8
+ spec.authors = ["donny741"]
9
+
10
+ spec.summary = "Ruby LSP FactoryBot Addon"
11
+ spec.description = "Provides go to definition and completion for FactoryBot attributes in Ruby LSP"
12
+ spec.homepage = "https://github.com/donny741/ruby-lsp-factory_bot"
13
+ spec.required_ruby_version = ">= 3.0.0"
14
+
15
+ spec.metadata["homepage_uri"] = spec.homepage
16
+ spec.metadata["source_code_uri"] = spec.homepage
17
+
18
+ # Specify which files should be added to the gem when it is released.
19
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
20
+ spec.files = Dir.chdir(__dir__) do
21
+ `git ls-files -z`.split("\x0").reject do |f|
22
+ (File.expand_path(f) == __FILE__) ||
23
+ f.start_with?(
24
+ "bin/",
25
+ "test/",
26
+ "spec/",
27
+ "features/",
28
+ ".git",
29
+ ".circleci",
30
+ "appveyor",
31
+ "Gemfile",
32
+ "misc/",
33
+ "sorbet/"
34
+ )
35
+ end
36
+ end
37
+ spec.bindir = "exe"
38
+ spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
39
+ spec.require_paths = ["lib"]
40
+
41
+ # Uncomment to register a new dependency of your gem
42
+ # spec.add_dependency "example-gem", "~> 1.0"
43
+ spec.add_dependency("ruby-lsp", ">= 0.23.0", "< 0.24.0")
44
+
45
+ # For more information and examples about making a new gem, check out our
46
+ # guide at: https://bundler.io/guides/creating_gem.html
47
+ end
@@ -0,0 +1,6 @@
1
+ module RubyLsp
2
+ module FactoryBot
3
+ VERSION: String
4
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
5
+ end
6
+ end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruby-lsp-factory_bot
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.3.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - donny741
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 2025-01-18 00:00:00.000000000 Z
10
+ date: 2025-03-24 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: ruby-lsp
@@ -34,7 +34,19 @@ description: Provides go to definition and completion for FactoryBot attributes
34
34
  executables: []
35
35
  extensions: []
36
36
  extra_rdoc_files: []
37
- files: []
37
+ files:
38
+ - ".rspec"
39
+ - ".rubocop.yml"
40
+ - README.md
41
+ - Rakefile
42
+ - lib/ruby-lsp-factory_bot.rb
43
+ - lib/ruby_lsp/factory_bot/addon.rb
44
+ - lib/ruby_lsp/factory_bot/definition.rb
45
+ - lib/ruby_lsp/factory_bot/indexing_enhancement.rb
46
+ - lib/ruby_lsp/factory_bot/utils.rb
47
+ - lib/ruby_lsp/factory_bot/version.rb
48
+ - ruby-lsp-factory_bot.gemspec
49
+ - sig/ruby_lsp/factory_bot.rbs
38
50
  homepage: https://github.com/donny741/ruby-lsp-factory_bot
39
51
  licenses: []
40
52
  metadata: