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 +4 -4
- data/.rubocop.yml +20 -0
- data/README.md +18 -0
- data/Rakefile +3 -1
- data/lib/ruby_lsp/rails/factory_bot/addon.rb +42 -22
- data/lib/ruby_lsp/rails/factory_bot/addon_name.rb +9 -0
- data/lib/ruby_lsp/rails/factory_bot/completion.rb +119 -81
- data/lib/ruby_lsp/rails/factory_bot/hover.rb +45 -47
- data/lib/ruby_lsp/rails/factory_bot/server_addon/attribute_handler.rb +69 -0
- data/lib/ruby_lsp/rails/factory_bot/server_addon/base_handler.rb +35 -0
- data/lib/ruby_lsp/rails/factory_bot/server_addon/factory_handler.rb +37 -0
- data/lib/ruby_lsp/rails/factory_bot/server_addon/trait_handler.rb +51 -0
- data/lib/ruby_lsp/rails/factory_bot/server_addon.rb +45 -0
- data/lib/ruby_lsp/rails/factory_bot.rb +14 -0
- data/lib/ruby_lsp_rails_factory_bot.rb +2 -1
- metadata +28 -8
- data/lib/ruby_lsp/rails/factory_bot/server_extension.rb +0 -131
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 01d1b4f936612100587a05ace6ca2c46fd654484c7d1125245e3c3afd7ce2fb2
|
4
|
+
data.tar.gz: cd15946860a27dbaa31ba14b8f429c5afb7f4bd21a9eeb1aca0f01411006bebf
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|

|
22
22
|
|
23
|
+
Receive completion suggestions as you type
|
24
|
+
|
25
|
+

|
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
@@ -1,37 +1,45 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require "ruby_lsp/addon"
|
4
|
-
require "ruby_lsp_rails/
|
4
|
+
require "ruby_lsp/ruby_lsp_rails/addon"
|
5
5
|
|
6
6
|
require_relative "completion"
|
7
7
|
require_relative "hover"
|
8
|
-
require_relative "
|
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
|
-
|
27
|
+
FactoryBot::ADDON_NAME
|
22
28
|
end
|
23
29
|
|
24
|
-
def create_completion_listener(response_builder, node_context, dispatcher,
|
25
|
-
|
26
|
-
|
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
|
-
|
34
|
-
|
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
|
-
|
50
|
+
runner_client.trigger_reload
|
43
51
|
end
|
44
52
|
|
45
53
|
private
|
46
54
|
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
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
|
-
|
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
|
@@ -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
|
-
|
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
|
-
|
29
|
-
|
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
|
-
|
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
|
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
|
-
|
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 =>
|
39
|
-
|
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
|
-
|
42
|
-
|
43
|
-
|
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
|
-
|
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
|
60
|
-
|
61
|
-
|
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
|
-
|
68
|
-
|
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
|
82
|
-
|
83
|
-
|
84
|
-
|
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 <<
|
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
|
-
|
107
|
-
|
108
|
-
@response_builder <<
|
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
|
-
|
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 =>
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
in [Prism::SymbolNode =>
|
51
|
-
|
52
|
-
|
53
|
-
handle_attribute(symbol_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 =
|
65
|
-
|
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
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
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 =
|
106
|
-
|
107
|
-
|
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
|
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.
|
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:
|
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.
|
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.
|
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: :
|
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/
|
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
|