ruby-lsp-rails-factory-bot 0.1.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 41ed188af5e2b4b289442274d6538f7105ce3d0e93dd7340923b7f752c7b042b
4
- data.tar.gz: a8b258fb2f0c05d17f614d3614515da65b29c04c95c17d7a39c2cd13bc1df261
3
+ metadata.gz: 01d1b4f936612100587a05ace6ca2c46fd654484c7d1125245e3c3afd7ce2fb2
4
+ data.tar.gz: cd15946860a27dbaa31ba14b8f429c5afb7f4bd21a9eeb1aca0f01411006bebf
5
5
  SHA512:
6
- metadata.gz: dbcb7349c449d4d33076e65c04060ca179c150a204d9d6ce93b274bdd5f25357c98a32263a98092943f7845086d8117266394af00f086db21750bf4d83963f92
7
- data.tar.gz: a2053e5de42267b86d927e7e0aa3fd4ab99475d9d82843086d4d91b1bb08dfa3efccf1a28aa79fbfd17990ee8d5bb330f99418590a561e878003b728f8ba39be
6
+ metadata.gz: e09bd8301b1003c30ba6efdba15c6c945f4f0ca0adbda81e8f290bcf4a0dff480c1c482e28499a5477d80a656a358018788a6a5022441da8698c940272089e2c
7
+ data.tar.gz: 3b145ca533d21b291cd545dde273f4c6f333ea255beca536c01f44e488b02a813d4beab114a4d4269b3c0add785b020df3cc875777d28183b2f338a7005a38df
data/.rubocop.yml CHANGED
@@ -1,8 +1,28 @@
1
1
  AllCops:
2
2
  TargetRubyVersion: 3.0
3
+ NewCops: enable
4
+ Exclude:
5
+ - '**/*.gemspec'
6
+ - '**/vendor/bundle/**/*'
7
+
8
+ Metrics/BlockLength:
9
+ Exclude:
10
+ - 'spec/**/*_spec.rb'
11
+
12
+ Style/RedundantReturn:
13
+ AllowMultipleReturnValues: true
3
14
 
4
15
  Style/StringLiterals:
5
16
  EnforcedStyle: double_quotes
6
17
 
7
18
  Style/StringLiteralsInInterpolation:
8
19
  EnforcedStyle: double_quotes
20
+
21
+ Style/TrailingCommaInHashLiteral:
22
+ EnforcedStyleForMultiline: consistent_comma
23
+
24
+ Style/TrailingCommaInArrayLiteral:
25
+ EnforcedStyleForMultiline: consistent_comma
26
+
27
+ Style/TrailingCommaInArguments:
28
+ EnforcedStyleForMultiline: consistent_comma
data/README.md CHANGED
@@ -20,6 +20,24 @@ Hover over an attribute or factory definition
20
20
 
21
21
  ![lsp-factory-bot-hover](https://github.com/user-attachments/assets/6f570288-3cf3-4d12-acf9-71c86e834cd8)
22
22
 
23
+ Receive completion suggestions as you type
24
+
25
+ ![lsp-factory-bot-completion](https://github.com/user-attachments/assets/4255a86a-8f36-4de2-8d10-8cb5a3f49e50)
26
+
27
+ ### Supports
28
+
29
+ | | Hover | Completion | Go to definition |
30
+ | ------------- |-------------| -----| ----|
31
+ | Factory name | ✅ | ⭕️ | ❌ |
32
+ | Trait | ✅ | ⭕️ | ❌ |
33
+ | Attribute | ✅ | ✅ | ❌ |
34
+
35
+ Notes:
36
+
37
+ - The extension has "understanding" of factory/trait completion items, but due to limitations on when ruby-lsp displays the completion suggestions, they aren't visible for Symbols (eg. factory/trait names) :/ though they happen to be visible for symbols in Hash/Kw notation (ie with `:` after - `key: ...`)
38
+ - Go to definition not supported yet but might come soon
39
+
40
+
23
41
  ## Development
24
42
 
25
43
  After checking out the repo, run `bin/setup` to install dependencies. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
data/Rakefile CHANGED
@@ -2,7 +2,9 @@
2
2
 
3
3
  require "bundler/gem_tasks"
4
4
  require "rubocop/rake_task"
5
+ require "rspec/core/rake_task"
5
6
 
6
7
  RuboCop::RakeTask.new
8
+ RSpec::Core::RakeTask.new(:spec)
7
9
 
8
- task default: :rubocop
10
+ task default: %i[rubocop spec]
@@ -1,37 +1,45 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "ruby_lsp/addon"
4
- require "ruby_lsp_rails/runner_client"
4
+ require "ruby_lsp/ruby_lsp_rails/addon"
5
5
 
6
6
  require_relative "completion"
7
7
  require_relative "hover"
8
- require_relative "server_extension"
8
+ require_relative "addon_name"
9
+ require_relative "../factory_bot"
10
+ require_relative "../../../ruby_lsp_rails_factory_bot"
9
11
 
10
12
  module RubyLsp
11
13
  module Rails
12
14
  module FactoryBot
15
+ # The addon to be registered with ruby-lsp. See https://shopify.github.io/ruby-lsp/add-ons.html
13
16
  class Addon < ::RubyLsp::Addon
14
- def activate(global_state, *)
17
+ def activate(global_state, outgoing_queue)
15
18
  @ruby_index = global_state.index
19
+
20
+ @outgoing_queue = outgoing_queue
21
+ log "Activating #{name} add-on v#{VERSION}"
16
22
  end
17
23
 
18
24
  def deactivate(*); end
19
25
 
20
26
  def name
21
- "ruby-lsp-rails-factory-bot"
27
+ FactoryBot::ADDON_NAME
22
28
  end
23
29
 
24
- def create_completion_listener(response_builder, node_context, dispatcher, uri)
25
- path = uri.to_standardized_path
26
- return unless path&.end_with?("_test.rb") || path&.end_with?("_spec.rb")
27
- return unless factory_bot_call_args?(node_context)
28
-
29
- Completion.new(response_builder, node_context, dispatcher, RubyLsp::Rails::RunnerClient.instance)
30
+ def create_completion_listener(response_builder, node_context, dispatcher, _uri)
31
+ register_addon!
32
+ Completion.new(response_builder, node_context, dispatcher, runner_client)
30
33
  end
31
34
 
35
+ # TODO: need URI param to be able to filter by file name
32
36
  def create_hover_listener(response_builder, node_context, dispatcher)
33
- # TODO: need URI param
34
- Hover.new(response_builder, node_context, dispatcher, RubyLsp::Rails::RunnerClient.instance, @ruby_index)
37
+ unless @addon_registered
38
+ register_addon!
39
+ return
40
+ end
41
+
42
+ Hover.new(response_builder, node_context, dispatcher, runner_client, @ruby_index)
35
43
  end
36
44
 
37
45
  def workspace_did_change_watched_files(changes)
@@ -39,21 +47,33 @@ module RubyLsp
39
47
  change[:uri].match?(/(?:spec|test).+factor.+\.rb/)
40
48
  end
41
49
 
42
- RubyLsp::Rails::RunnerClient.instance.trigger_reload
50
+ runner_client.trigger_reload
43
51
  end
44
52
 
45
53
  private
46
54
 
47
- FACTORY_BOT_METHODS = %i[
48
- create
49
- build
50
- build_stubbed
51
- attributes_for
52
- ].flat_map { |attr| [attr, :"#{attr}_list", :"#{attr}_pair"] }.freeze
55
+ # the addon must be registered as a rails server addon once the server has booted
56
+ def register_addon!
57
+ @addon_registered ||= # rubocop:disable Naming/MemoizedInstanceVariableName
58
+ begin
59
+ addon_path = File.expand_path("server_addon.rb", __dir__)
60
+ runner_client.register_server_addon(addon_path)
61
+ true
62
+ end
63
+ end
64
+
65
+ def runner_client
66
+ @rails_addon ||= ::RubyLsp::Addon.get(
67
+ "Ruby LSP Rails",
68
+ ::RubyLsp::Rails::FactoryBot::REQUIRED_RUBY_LSP_RAILS_VERSION,
69
+ )
70
+ @rails_addon.rails_runner_client
71
+ end
72
+
73
+ def log(msg)
74
+ return if !@outgoing_queue || @outgoing_queue.closed?
53
75
 
54
- def factory_bot_call_args?(node_context)
55
- node_context.call_node && FACTORY_BOT_METHODS.include?(node_context.call_node.name)
56
- true
76
+ @outgoing_queue << Notification.window_log_message(msg)
57
77
  end
58
78
  end
59
79
  end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLsp
4
+ module Rails
5
+ module FactoryBot
6
+ ADDON_NAME = "ruby-lsp-rails-factory-bot"
7
+ end
8
+ end
9
+ end
@@ -2,20 +2,15 @@
2
2
 
3
3
  require "ruby_lsp/internal"
4
4
 
5
+ require_relative "addon_name"
6
+
5
7
  module RubyLsp
6
8
  module Rails
7
9
  module FactoryBot
8
- class Completion
10
+ # The listener that is created when the user requests autocomplete at the relevant time.
11
+ class Completion # rubocop:disable Metrics/ClassLength
9
12
  include RubyLsp::Requests::Support::Common
10
13
 
11
- # TODO: Avoid duplication with other class
12
- FACTORY_BOT_METHODS = %i[
13
- create
14
- build
15
- build_stubbed
16
- attributes_for
17
- ].flat_map { |attr| [attr, :"#{attr}_list", :"#{attr}_pair"] }.freeze
18
-
19
14
  def initialize(response_builder, node_context, dispatcher, server_client)
20
15
  @response_builder = response_builder
21
16
  @node_context = node_context
@@ -25,103 +20,146 @@ module RubyLsp
25
20
  end
26
21
 
27
22
  def on_call_node_enter(node)
28
- return unless FACTORY_BOT_METHODS.include?(node.name) ||
29
- FACTORY_BOT_METHODS.include?(@node_context.parent.name)
23
+ call_node = @node_context.call_node
24
+ return unless call_node
25
+
26
+ return unless FactoryBot::FACTORY_BOT_METHODS.include?(call_node.name)
27
+
28
+ process_arguments_pattern(node, call_node.arguments&.arguments)
29
+ rescue StandardError => e
30
+ $stderr.write(e, e.backtrace)
31
+ end
30
32
 
31
- case node.arguments
33
+ private
34
+
35
+ def process_arguments_pattern(node, arguments) # rubocop:disable Metrics/MethodLength
36
+ case arguments
32
37
  in [Prism::SymbolNode => factory_name_node]
33
- handle_factory(factory_name_node, factory_name_node.value.to_s)
38
+ handle_factory(factory_name_node, node_string_value(factory_name_node))
34
39
 
35
40
  in [Prism::SymbolNode => factory_name_node, *, Prism::SymbolNode => trait_node]
36
- handle_trait(factory_name_node.value.to_s, node, trait_node.value.to_s)
41
+ already_used_traits = gather_already_used_traits(arguments)
42
+ handle_trait(
43
+ node_string_value(factory_name_node), node, already_used_traits, node_string_value(trait_node),
44
+ )
37
45
 
38
- in [Prism::SymbolNode => factory_name_node, *, Prism::CallNode => call_node]
39
- handle_attribute(factory_name_node.value.to_s, node, call_node.message)
46
+ in [Prism::SymbolNode => _factory_name_node, *, Prism::KeywordHashNode => _kw_node] |
47
+ [Prism::SymbolNode => _factory_name_node, *, Prism::HashNode => _kw_node] |
48
+ [Prism::SymbolNode => _factory_name_node, *, Prism::CallNode => _call_node]
40
49
 
41
- in [Prism::SymbolNode => factory_name_node, *, Prism::KeywordHashNode => kw_node]
42
- handle_attribute(
43
- factory_name_node.value.to_s, node, kw_node.elements.last.key.value&.to_s
44
- )
45
- in [Prism::SymbolNode => factory_name_node, *, Prism::HashNode => hash_node]
46
- handle_attribute(
47
- factory_name_node.value.to_s, node, hash_node.elements.last.key.value&.to_s
48
- )
50
+ attr_name = node_string_value(_call_node || _kw_node.elements.last.key)
51
+ already_used_attrs = gather_already_used_attrs(_kw_node)
52
+ handle_attribute(node_string_value(_factory_name_node), node, already_used_attrs, attr_name)
49
53
  else
50
- $stderr.write node.arguments
51
54
  nil
52
55
  end
53
- rescue => e
54
- $stderr.write(e, e.backtrace)
55
56
  end
56
57
 
57
- private
58
+ def node_string_value(node)
59
+ case node
60
+ when Prism::CallNode
61
+ node.name.to_s
62
+ when Prism::SymbolNode
63
+ node.value.to_s
64
+ when nil
65
+ ""
66
+ end
67
+ end
58
68
 
59
- def handle_attribute(factory_name, node, value = "")
60
- @server_client.get_attributes(factory_name: factory_name, name: value)&.each do |attr|
61
- $stderr.write "attribute", attr
62
- label_details = Interface::CompletionItemLabelDetails.new(
63
- description: attr[:type],
64
- )
65
- range = range_from_node(node)
69
+ def gather_already_used_attrs(kw_node)
70
+ attrs = Set.new
71
+ return attrs unless kw_node
66
72
 
67
- @response_builder << Interface::CompletionItem.new(
68
- label: attr[:name],
69
- filter_text: attr[:name],
70
- label_details: label_details,
71
- text_edit: Interface::TextEdit.new(range: range, new_text: attr[:name]),
72
- kind: Constant::CompletionItemKind::PROPERTY,
73
- data: {
74
- owner_name: attr[:owner],
75
- guessed_type: attr[:owner], # the type of the owner, not the attribute
76
- },
77
- )
73
+ kw_node.elements.each do |e|
74
+ attrs.add(node_string_value(e.key))
78
75
  end
76
+
77
+ attrs
79
78
  end
80
79
 
81
- def handle_trait(factory_name, node, value = "")
82
- @server_client.get_traits(factory_name: factory_name, name: value)&.each do |tr|
83
- $stderr.write "trait", tr
84
- label_details = Interface::CompletionItemLabelDetails.new(
85
- description: tr[:owner],
86
- )
87
- range = range_from_node(node)
80
+ def handle_attribute(factory_name, node, already_used_attrs, value = "")
81
+ range = range_from_node(node)
82
+ make_request(:attributes, factory_name: factory_name, name: value)&.each do |attr|
83
+ next if already_used_attrs.member?(attr[:name].to_s)
88
84
 
85
+ label_details = Interface::CompletionItemLabelDetails.new(description: attr[:type])
86
+
87
+ @response_builder << serialise_attribute(attr[:name], label_details, attr[:owner], range)
88
+ end
89
+ end
90
+
91
+ def serialise_attribute(name, label_details, owner, range)
92
+ Interface::CompletionItem.new(
93
+ label: name,
94
+ filter_text: name,
95
+ label_details: label_details,
96
+ text_edit: Interface::TextEdit.new(range: range, new_text: name),
97
+ kind: Constant::CompletionItemKind::PROPERTY,
98
+ data: { owner_name: owner, guessed_type: owner }, # the type of the owner, not the attribute
99
+ )
100
+ end
101
+
102
+ def gather_already_used_traits(arguments)
103
+ trait_names = Set.new
104
+ # skip the first one because it's factory name
105
+ 1.upto(arguments.length - 1) do |i|
106
+ arg = arguments[i]
107
+ next if arg.is_a?(Prism::IntegerNode)
108
+ break unless arg.is_a?(Prism::SymbolNode)
109
+
110
+ trait_names.add(arg.value.to_s)
111
+ end
112
+ trait_names
113
+ end
114
+
115
+ def handle_trait(factory_name, node, already_used_traits, value = "")
116
+ make_request(:traits, factory_name: factory_name, name: value)&.each do |tr|
117
+ next if already_used_traits.member?(tr[:name].to_s)
118
+
119
+ label_details = Interface::CompletionItemLabelDetails.new(description: tr[:owner])
120
+ range = range_from_node(node)
89
121
  name = tr[:name]
90
122
 
91
- @response_builder << Interface::CompletionItem.new(
92
- label: name,
93
- filter_text: name,
94
- label_details: label_details,
95
- text_edit: Interface::TextEdit.new(range: range, new_text: name),
96
- kind: Constant::CompletionItemKind::PROPERTY,
97
- data: {
98
- owner_name: nil,
99
- guessed_type: tr[:owner] # the type of the owner
100
- },
101
- )
123
+ @response_builder << serialise_trait(name, range, label_details, tr[:owner])
102
124
  end
103
125
  end
104
126
 
127
+ def serialise_trait(name, range, label_details, owner)
128
+ Interface::CompletionItem.new(
129
+ label: name,
130
+ filter_text: name,
131
+ label_details: label_details,
132
+ text_edit: Interface::TextEdit.new(range: range, new_text: name),
133
+ kind: Constant::CompletionItemKind::PROPERTY,
134
+ data: { owner_name: nil, guessed_type: owner },
135
+ )
136
+ end
137
+
105
138
  def handle_factory(node, name)
106
- @server_client.get_factories(name: name)&.each do |fact|
107
- $stderr.write "factory", fact
108
- @response_builder << Interface::CompletionItem.new(
109
- label: fact[:name],
110
- filter_text: fact[:name],
111
- label_details: Interface::CompletionItemLabelDetails.new(
112
- description: fact[:model_class]
113
- ),
114
- text_edit: Interface::TextEdit.new(
115
- range: range_from_node(node),
116
- new_text: fact[:name],
117
- ),
118
- kind: Constant::CompletionItemKind::CLASS,
119
- data: {
120
- guessed_type: fact[:model_class]
121
- }
122
- )
139
+ range = range_from_node(node)
140
+ make_request(:factories, name: name)&.each do |fact|
141
+ @response_builder << serialise_factory(fact[:name], fact[:model_class], range)
123
142
  end
124
143
  end
144
+
145
+ def make_request(request_name, **params)
146
+ @server_client.delegate_request(
147
+ server_addon_name: FactoryBot::ADDON_NAME,
148
+ request_name: request_name.to_s,
149
+ **params,
150
+ )
151
+ end
152
+
153
+ def serialise_factory(name, model_class, range)
154
+ Interface::CompletionItem.new(
155
+ label: name,
156
+ filter_text: name,
157
+ label_details: Interface::CompletionItemLabelDetails.new(description: model_class),
158
+ text_edit: Interface::TextEdit.new(range: range, new_text: name),
159
+ kind: Constant::CompletionItemKind::CLASS,
160
+ data: { guessed_type: model_class },
161
+ )
162
+ end
125
163
  end
126
164
  end
127
165
  end
@@ -2,20 +2,15 @@
2
2
 
3
3
  require "ruby_lsp/internal"
4
4
 
5
+ require_relative "addon_name"
6
+
5
7
  module RubyLsp
6
8
  module Rails
7
9
  module FactoryBot
10
+ # The listener that is created during relevant hover actions
8
11
  class Hover
9
12
  include RubyLsp::Requests::Support::Common
10
13
 
11
- # TODO: Avoid duplication with other class
12
- FACTORY_BOT_METHODS = %i[
13
- create
14
- build
15
- build_stubbed
16
- attributes_for
17
- ].flat_map { |attr| [attr, :"#{attr}_list", :"#{attr}_pair"] }.freeze
18
-
19
14
  def initialize(response_builder, node_context, dispatcher, server_client, ruby_index)
20
15
  @response_builder = response_builder
21
16
  @node_context = node_context
@@ -34,83 +29,86 @@ module RubyLsp
34
29
  # "parent" isn't strictly speaking the immediate parent as in the AST - the
35
30
  # element it refers to is a bit opinionated... in this case, it is always the call node,
36
31
  # whether the symbol is an argument in the call_node args, or a symbol in a kw hash :/
37
- unless parent.is_a?(Prism::CallNode) || FACTORY_BOT_METHODS.include?(call_node.message.to_sym)
32
+ unless parent.is_a?(Prism::CallNode) || FactoryBot::FACTORY_BOT_METHODS.include?(call_node.message.to_sym)
38
33
  return
39
34
  end
40
35
 
41
- case call_node.arguments.arguments
36
+ process_arguments_pattern(symbol_node, call_node.arguments.arguments)
37
+ end
38
+
39
+ private
40
+
41
+ def process_arguments_pattern(symbol_node, arguments) # rubocop:disable Metrics/MethodLength
42
+ case arguments
42
43
  in [^symbol_node, *]
43
44
  handle_factory(symbol_node)
44
- in [Prism::SymbolNode => factory_node, *, ^symbol_node]
45
- handle_trait(symbol_node, factory_node)
46
- in [Prism::SymbolNode => factory_node, ^symbol_node, *]
47
- handle_trait(symbol_node, factory_node)
48
- in [Prism::SymbolNode => factory_node, Integer, ^symbol_node, *]
49
- handle_trait(symbol_node, factory_node)
50
- in [Prism::SymbolNode => factory_node, *, Prism::KeywordHashNode => kw_hash] if kw_hash.elements.any? { |e| e.key == symbol_node }
51
- handle_attribute(symbol_node, factory_node)
52
- in [Prism::SymbolNode => factory_node, *, Prism::HashNode => kw_hash] if kw_hash.elements.any? { |e| e.key == symbol_node }
53
- handle_attribute(symbol_node, factory_node)
45
+ in [Prism::SymbolNode => _factory_node, *, ^symbol_node] |
46
+ [Prism::SymbolNode => _factory_node, ^symbol_node, *] |
47
+ [Prism::SymbolNode => _factory_node, Integer, ^symbol_node, *]
48
+
49
+ handle_trait(symbol_node, _factory_node)
50
+
51
+ in [Prism::SymbolNode => _factory_node, *, Prism::KeywordHashNode => _kw_hash] |
52
+ [Prism::SymbolNode => _factory_node, *, Prism::HashNode => _kw_hash]
53
+
54
+ handle_attribute(symbol_node, _factory_node) if _kw_hash.elements.any? { |e| e.key == symbol_node }
54
55
  else
55
- $stderr.write "nope fact_or_trait", call_node.arguments.arguments
56
56
  nil
57
57
  end
58
58
  end
59
59
 
60
- private
61
-
62
60
  def handle_attribute(symbol_node, factory_node)
63
61
  name = symbol_node.value.to_s
64
- attribute = @server_client.get_attributes(
65
- factory_name: factory_node.value.to_s, name: name
62
+ attribute = make_request(
63
+ :attributes,
64
+ factory_name: factory_node.value.to_s, name: name,
66
65
  )&.find { |attr| attr[:name] == name }
67
66
 
68
67
  return unless attribute
69
68
 
70
69
  @response_builder.push(
71
70
  "#{attribute[:name]} (#{attribute[:type]})",
72
- category: :documentation
71
+ category: :documentation,
73
72
  )
74
73
  end
75
74
 
76
75
  def handle_factory(symbol_node)
77
76
  name = symbol_node.value.to_s
78
-
79
- factory = @server_client.get_factories(name: name)&.find { |f| f[:name] == name }
80
-
77
+ factory = make_request(:factories, name: name)&.find { |f| f[:name] == name }
81
78
  return unless factory
82
79
 
83
80
  index_entry = @ruby_index.first_unqualified_const(factory[:name])
84
81
 
85
- if index_entry
86
- @response_builder.push(
87
- markdown_from_index_entries(
88
- factory[:model_class],
89
- index_entry
90
- ),
91
- category: :documentation
92
- )
93
- else
94
- @response_builder.push(
95
- "#{factory[:name]} (#{factory[:model_class]})",
96
- category: :documentation
97
- )
98
- end
82
+ hint = if index_entry
83
+ markdown_from_index_entries(factory[:model_class], index_entry)
84
+ else
85
+ "#{factory[:name]} (#{factory[:model_class]})"
86
+ end
87
+
88
+ @response_builder.push(hint, category: :documentation)
99
89
  end
100
90
 
101
91
  def handle_trait(symbol_node, factory_node)
102
92
  factory_name = factory_node.value.to_s
103
93
  trait_name = symbol_node.value.to_s
104
94
 
105
- trait = @server_client.get_traits(
106
- factory_name: factory_name, name: trait_name
107
- )&.find { |tr| tr[:name] == trait_name }
95
+ trait = make_request(:traits, factory_name: factory_name, name: trait_name)&.find do |tr|
96
+ tr[:name] == trait_name
97
+ end
108
98
 
109
99
  return unless trait
110
100
 
111
101
  @response_builder.push(
112
102
  "#{trait[:name]} (trait of #{trait[:owner] || factory_name})",
113
- category: :documentation
103
+ category: :documentation,
104
+ )
105
+ end
106
+
107
+ def make_request(request_name, **params)
108
+ @server_client.delegate_request(
109
+ server_addon_name: FactoryBot::ADDON_NAME,
110
+ request_name: request_name.to_s,
111
+ **params,
114
112
  )
115
113
  end
116
114
  end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base_handler"
4
+
5
+ module RubyLsp
6
+ module Rails
7
+ module FactoryBot
8
+ class ServerAddon < RubyLsp::Rails::ServerAddon
9
+ # Handler for fetching and serialising traits
10
+ class AttributeHandler < BaseHandler
11
+ private
12
+
13
+ def fetch(params)
14
+ factory = ::FactoryBot.factories[params[:factory_name]]
15
+ name = params[:name]
16
+
17
+ attributes = factory.definition.declarations.select do |attr|
18
+ name.nil? || name.empty? ? true : attr.name.to_s.include?(name)
19
+ end
20
+ model_class = factory.send :class_name
21
+
22
+ return attributes, model_class
23
+ rescue KeyError
24
+ # FactoryBot throws a KeyError if the factory isn't found, so nothing to do here
25
+ end
26
+
27
+ def serialise(attributes, model_class)
28
+ attributes.map do |attribute|
29
+ source_location = block_for(attribute)&.source_location
30
+ {
31
+ name: attribute.name,
32
+ owner: model_class.name,
33
+ type: guess_attribute_type(attribute, model_class),
34
+ source_location: source_location,
35
+ source: block_source(attribute),
36
+ }
37
+ end
38
+ end
39
+
40
+ def guess_attribute_type(attribute, model_class)
41
+ if model_class.respond_to? :attribute_types
42
+ type = model_class.attribute_types[attribute.name.to_s]
43
+ return nil if type.nil?
44
+
45
+ return ACTIVE_MODEL_TYPE_TO_RUBY_TYPE[type.class] || type.type
46
+ end
47
+
48
+ return unless model_class.respond_to? :reflections
49
+
50
+ association = model_class.reflections[attribute.name.to_s]
51
+
52
+ association&.klass
53
+ end
54
+
55
+ ACTIVE_MODEL_TYPE_TO_RUBY_TYPE = {
56
+ ActiveModel::Type::String => "String",
57
+ ActiveModel::Type::ImmutableString => "String",
58
+ ActiveModel::Type::Float => "Float",
59
+ ActiveModel::Type::Integer => "Integer",
60
+ ActiveModel::Type::Boolean => "boolean",
61
+ ActiveModel::Type::Date => "Date",
62
+ ActiveModel::Type::Time => "Time",
63
+ ActiveModel::Type::DateTime => "DateTime",
64
+ }.freeze
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLsp
4
+ module Rails
5
+ module FactoryBot
6
+ class ServerAddon < RubyLsp::Rails::ServerAddon
7
+ # Common handler functionality
8
+ class BaseHandler
9
+ def execute(params)
10
+ collection, *rest = fetch(params)
11
+
12
+ serialise(collection, *rest) if collection
13
+ end
14
+
15
+ private
16
+
17
+ def fetch(_params) = throw("Not implemented")
18
+
19
+ def serialise(_collection, *) = throw("Not implemented")
20
+
21
+ # helper - might be best to live elsewhere?
22
+ def block_for(attr)
23
+ attr.instance_variable_get :@block
24
+ end
25
+
26
+ # helper - might be best to live elsewhere?
27
+ def block_source(attr)
28
+ blk = block_for(attr)
29
+ blk.source if blk.respond_to? :source
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base_handler"
4
+
5
+ module RubyLsp
6
+ module Rails
7
+ module FactoryBot
8
+ class ServerAddon < RubyLsp::Rails::ServerAddon
9
+ # Handler for fetching and serialising factories
10
+ class FactoryHandler < BaseHandler
11
+ private
12
+
13
+ def fetch(params)
14
+ name = params[:name]
15
+ factories = ::FactoryBot.factories.select do |f|
16
+ name.nil? || name.empty? ? true : f.name.to_s.include?(params[:name])
17
+ end
18
+ return factories, nil
19
+ end
20
+
21
+ def serialise(factories, *)
22
+ factories.map do |fact|
23
+ model_class = fact.send :class_name
24
+
25
+ case model_class
26
+ when String, Symbol
27
+ model_class = model_class.to_s.camelize
28
+ end
29
+
30
+ { name: fact.name, model_class: model_class }
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base_handler"
4
+
5
+ module RubyLsp
6
+ module Rails
7
+ module FactoryBot
8
+ class ServerAddon < RubyLsp::Rails::ServerAddon
9
+ # Handler for fetching and serialising traits
10
+ class TraitHandler < BaseHandler
11
+ private
12
+
13
+ def fetch(params)
14
+ factory = ::FactoryBot.factories[params[:factory_name]]
15
+
16
+ trait_name_partial = params[:name]
17
+ defined_traits = defined_traits(factory, trait_name_partial)
18
+ internal_traits = internal_traits(factory.send(:class_name), trait_name_partial)
19
+
20
+ traits = defined_traits.concat(internal_traits)
21
+ return traits, nil
22
+ rescue KeyError
23
+ # FactoryBot throws a KeyError if the factory isn't found, so nothing to do here
24
+ end
25
+
26
+ def defined_traits(factory, trait_name_partial)
27
+ factory.defined_traits.select { |tr| tr.name.to_s.include? trait_name_partial }
28
+ end
29
+
30
+ def internal_traits(factory_class_name, trait_name_partial)
31
+ ::FactoryBot::Internal.traits.select do |tr|
32
+ (tr.klass == factory_class_name || !tr.klass) && tr.name.to_s.include?(trait_name_partial)
33
+ end
34
+ end
35
+
36
+ def serialise(traits, *)
37
+ traits.map do |tr|
38
+ source_location = block_for(tr)&.source_location
39
+ {
40
+ name: tr.name,
41
+ source_location: source_location,
42
+ source: block_source(tr),
43
+ owner: tr.klass,
44
+ }
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "addon_name"
4
+ require_relative "server_addon/factory_handler"
5
+ require_relative "server_addon/trait_handler"
6
+ require_relative "server_addon/attribute_handler"
7
+
8
+ module RubyLsp
9
+ module Rails
10
+ module FactoryBot
11
+ # The addon for the ruby-lsp-rails server runtime
12
+ class ServerAddon < RubyLsp::Rails::ServerAddon
13
+ def initialize(stdout, stderr, capabilities)
14
+ super
15
+
16
+ # TODO: move to before_start hook
17
+ with_progress "ruby-lsp-rails-factory-bot-1", "initialisation" do
18
+ require "factory_bot"
19
+ ::FactoryBot.find_definitions
20
+ ::FactoryBot.factories.each(&:compile)
21
+ end
22
+ end
23
+
24
+ def name = FactoryBot::ADDON_NAME
25
+
26
+ def execute(request, params) # rubocop:disable Metrics/MethodLength
27
+ case request.to_sym
28
+ when :factories
29
+ collection = FactoryHandler.new.execute(params)
30
+ when :traits
31
+ collection = TraitHandler.new.execute(params)
32
+ when :attributes
33
+ collection = AttributeHandler.new.execute(params)
34
+ else
35
+ return send_error_response("#{request} no supported")
36
+ end
37
+
38
+ send_result(collection || [])
39
+ rescue => e # rubocop:disable Style/RescueStandardError
40
+ send_error_response("An error occurred while fetching #{request} - #{e}")
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLsp
4
+ module Rails
5
+ module FactoryBot
6
+ FACTORY_BOT_METHODS = %i[
7
+ create
8
+ build
9
+ build_stubbed
10
+ attributes_for
11
+ ].flat_map { |attr| [attr, :"#{attr}_list", :"#{attr}_pair"] }.freeze
12
+ end
13
+ end
14
+ end
@@ -3,7 +3,8 @@
3
3
  module RubyLsp
4
4
  module Rails
5
5
  module FactoryBot
6
- VERSION = "0.1.0".freeze
6
+ VERSION = "0.3.0"
7
+ REQUIRED_RUBY_LSP_RAILS_VERSION = "~> 0.4"
7
8
  end
8
9
  end
9
10
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruby-lsp-rails-factory-bot
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - johansenja
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-09-11 00:00:00.000000000 Z
11
+ date: 2025-02-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: factory_bot
@@ -30,22 +30,36 @@ dependencies:
30
30
  requirements:
31
31
  - - "~>"
32
32
  - !ruby/object:Gem::Version
33
- version: 0.17.17
33
+ version: '0.23'
34
34
  type: :runtime
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
38
  - - "~>"
39
39
  - !ruby/object:Gem::Version
40
- version: 0.17.17
40
+ version: '0.23'
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: ruby-lsp-rails
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '0.4'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '0.4'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rspec
43
57
  requirement: !ruby/object:Gem::Requirement
44
58
  requirements:
45
59
  - - ">="
46
60
  - !ruby/object:Gem::Version
47
61
  version: '0'
48
- type: :runtime
62
+ type: :development
49
63
  prerelease: false
50
64
  version_requirements: !ruby/object:Gem::Requirement
51
65
  requirements:
@@ -64,18 +78,24 @@ files:
64
78
  - LICENSE.txt
65
79
  - README.md
66
80
  - Rakefile
81
+ - lib/ruby_lsp/rails/factory_bot.rb
67
82
  - lib/ruby_lsp/rails/factory_bot/addon.rb
83
+ - lib/ruby_lsp/rails/factory_bot/addon_name.rb
68
84
  - lib/ruby_lsp/rails/factory_bot/completion.rb
69
85
  - lib/ruby_lsp/rails/factory_bot/hover.rb
70
- - lib/ruby_lsp/rails/factory_bot/server_extension.rb
86
+ - lib/ruby_lsp/rails/factory_bot/server_addon.rb
87
+ - lib/ruby_lsp/rails/factory_bot/server_addon/attribute_handler.rb
88
+ - lib/ruby_lsp/rails/factory_bot/server_addon/base_handler.rb
89
+ - lib/ruby_lsp/rails/factory_bot/server_addon/factory_handler.rb
90
+ - lib/ruby_lsp/rails/factory_bot/server_addon/trait_handler.rb
71
91
  - lib/ruby_lsp_rails_factory_bot.rb
72
92
  - sig/ruby_lsp/rails/factory_bot.rbs
73
- homepage: https://github.com/johansenja/ruby-lsp-factory-bot
93
+ homepage: https://github.com/johansenja/ruby-lsp-rails-factory-bot
74
94
  licenses:
75
95
  - MIT
76
96
  metadata:
77
97
  allowed_push_host: https://rubygems.org
78
- homepage_uri: https://github.com/johansenja/ruby-lsp-factory-bot
98
+ homepage_uri: https://github.com/johansenja/ruby-lsp-rails-factory-bot
79
99
  source_code_uri: https://github.com/johansenja/ruby-lsp-rails-factory-bot
80
100
  changelog_uri: https://github.com/johansenja/ruby-lsp-rails-factory-bot
81
101
  post_install_message:
@@ -1,131 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "ruby_lsp_rails/server/extension"
4
-
5
- module RubyLsp
6
- module Rails
7
- module FactoryBot
8
- class ServerExtension < RubyLsp::Rails::Server::Extension
9
-
10
- before_start do
11
- require "factory_bot"
12
- ::FactoryBot.find_definitions
13
- ::FactoryBot.factories.each(&:compile)
14
- end
15
-
16
- after_reload do
17
- ::FactoryBot.find_definitions
18
- ::FactoryBot.factories.each(&:compile)
19
- end
20
-
21
- command :factories do |params|
22
- name = params[:name]
23
- factories = ::FactoryBot.factories.select do |f|
24
- name.nil? || name.empty? ? true : f.name.to_s.include?(params[:name])
25
- end
26
-
27
- factories.map do |fact|
28
- model_class = fact.send :class_name
29
- case model_class
30
- when String, Symbol
31
- model_class = model_class.to_s.camelize
32
- end
33
- {
34
- name: fact.name,
35
- model_class: model_class,
36
- }
37
- end
38
- end
39
-
40
- command :traits do |params|
41
- factory = ::FactoryBot.factories[params[:factory_name]]
42
-
43
- trait_name_partial = params[:name]
44
- traits = factory.defined_traits.select { |tr| tr.name.to_s.include? trait_name_partial }.concat(
45
- ::FactoryBot::Internal.traits.select { |tr|
46
- (tr.klass == factory.send(:class_name) || !tr.klass) && tr.name.to_s.include?(trait_name_partial)
47
- }
48
- )
49
-
50
- traits.map do |tr|
51
- source_location = ServerExtension.block_for(tr)&.source_location
52
- {
53
- name: tr.name,
54
- source_location: source_location,
55
- source: ServerExtension.block_source(tr),
56
- owner: tr.klass,
57
- }
58
- end
59
- rescue KeyError
60
- end
61
-
62
- command :attributes do |params|
63
- factory = ::FactoryBot.factories[params[:factory_name]]
64
- name = params[:name]
65
-
66
- attributes = factory.definition.declarations.select { |attr|
67
- name.nil? || name.empty? ? true : attr.name.to_s.include?(name)
68
- }
69
-
70
- model_class = factory.send :class_name
71
-
72
- attributes.map do |attribute|
73
- source_location = ServerExtension.block_for(attribute)&.source_location
74
- {
75
- name: attribute.name,
76
- owner: model_class.name,
77
- type: ServerExtension.guess_attribute_type(attribute, model_class),
78
- source_location: source_location,
79
- source: ServerExtension.block_source(attribute),
80
- }
81
- end
82
- rescue KeyError
83
- end
84
-
85
- class << self
86
- def block_for(attr)
87
- attr.instance_variable_get :@block
88
- end
89
-
90
- def block_source(attr)
91
- blk = block_for(attr)
92
-
93
- blk.source if blk.respond_to? :source
94
- end
95
-
96
- def guess_attribute_type(attribute, model_class)
97
- if model_class.respond_to? :attribute_types
98
- type = model_class.attribute_types[attribute.name.to_s]
99
-
100
- return case type
101
- when nil then nil
102
- when ActiveModel::Type::String, ActiveModel::Type::ImmutableString
103
- "String"
104
- when ActiveModel::Type::Float
105
- "Float"
106
- when ActiveModel::Type::Integer
107
- "Integer"
108
- when ActiveModel::Type::Boolean
109
- "boolean"
110
- when ActiveModel::Type::Date
111
- "Date"
112
- when ActiveModel::Type::Time
113
- "Time"
114
- when ActiveModel::Type::DateTime
115
- "DateTime"
116
- else
117
- type.type
118
- end
119
- end
120
-
121
- return unless model_class.respond_to? :reflections
122
-
123
- association = model_class.reflections[attribute.name.to_s]
124
-
125
- association&.klass
126
- end
127
- end
128
- end
129
- end
130
- end
131
- end