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 +4 -4
- data/.rspec +3 -0
- data/.rubocop.yml +8 -0
- data/README.md +26 -0
- data/Rakefile +12 -0
- data/lib/ruby-lsp-factory_bot.rb +10 -0
- data/lib/ruby_lsp/factory_bot/addon.rb +32 -0
- data/lib/ruby_lsp/factory_bot/definition.rb +134 -0
- data/lib/ruby_lsp/factory_bot/indexing_enhancement.rb +114 -0
- data/lib/ruby_lsp/factory_bot/utils.rb +18 -0
- data/lib/ruby_lsp/factory_bot/version.rb +7 -0
- data/ruby-lsp-factory_bot.gemspec +47 -0
- data/sig/ruby_lsp/factory_bot.rbs +6 -0
- metadata +15 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f4ae7bc8179ba4962a90e3adc74721712416deeceb295f410bf67e857c6e7f85
|
4
|
+
data.tar.gz: a5186ec32f38cf6013509c4deef814f1c21fe8da584972cca76c59d0c549e654
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 61d0eff2bd0fdca96578d1440e1182159cd2972a0bce5528e3d14616b984b1d51ac63f841622f88508359f9a99ad29eaeda74b872f2fc57fd790593241005e4b
|
7
|
+
data.tar.gz: 56e61ef4a900e2712a8c0e03307c8a19079733ef79382531dddc4bbfe4ac71fbc048ec946c03d4fef0343732daaaa1fe228b21ff15bd6a1c36de2e42c95fe0e2
|
data/.rspec
ADDED
data/.rubocop.yml
ADDED
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,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,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
|
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.
|
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-
|
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:
|