ruby-lsp-ree 0.1.6 → 0.1.8

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: 3814d8a258c4c291c8c2a858e5bf23094086e25162435b956bae4401bd456109
4
- data.tar.gz: a8ae59abe162776aba2356dc2a3c9b607703ac9d5012fac2c843ab84c7e12e96
3
+ metadata.gz: ccc9c39cc6ca44feeb3c8c5369e259a790dc01c02a0aecf9a14eee2d19e65015
4
+ data.tar.gz: 61233b0b379f7ab994f102fc748e2ef54eed53e605017bb0d28793ea160a45d7
5
5
  SHA512:
6
- metadata.gz: 4ab9db29ce9ed0fc7ee54721826ee1933308511c3f07ddce31c53504dde5d422a131dcb6421bb428c9694ddbbc6cdab83b5f96315d9195e97e2bac11a5bad2e2
7
- data.tar.gz: f6dcffe5363c8acb0481b9c8adbd1d6a5b6322833dc0fe1d8001188d7daf796aa68008ae648d3b99a4ad7f9f9e281b0fbdab4efec80a88b362cd18865dbd829c
6
+ metadata.gz: 07cf2e0c6c6670f5405a967e7fffe9af19af8a29dc55d33afef3b9dd0f231df826a0cd11d4e5b91ad23b7348635107c0186a7bb8b878bce00d9a480710267608
7
+ data.tar.gz: 5a5bcdf420fe934e46ec0b0318c6325cd5c6a0753ca18ea73a8188f54dc19a1c7abd6b3e41a777fb4f0a8b6f6bf83c8fd015226ceb21ee54c8228264874f0aaf
data/CHANGELOG.md CHANGED
@@ -1,3 +1,18 @@
1
+ ## [0.1.8] - 2025-03-21
2
+
3
+ - hover: show missing locales
4
+ - formatter: add raised error definition if missing
5
+ - formatter: add placeholder for missing locale
6
+ - formatter: improved adding raised errors into contract throw section
7
+ - Go To Definition: improved location for error locales
8
+ - Go To Definition: got to missing locale placeholder
9
+
10
+ ## [0.1.7] - 2025-03-14
11
+
12
+ - Go To Definition for ree errors into locales
13
+ - hover for ree error locales
14
+ - formatter: add raised ree errors to `throws` section
15
+
1
16
  ## [0.1.6] - 2025-03-04
2
17
 
3
18
  - file rename triggers class name change
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- ruby-lsp-ree (0.1.6)
4
+ ruby-lsp-ree (0.1.8)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
@@ -0,0 +1,13 @@
1
+ module RubyLsp
2
+ module Ree
3
+ class BaseFormatter
4
+ def self.call(source, uri)
5
+ new.call(source, uri)
6
+ end
7
+
8
+ def call(source, uri)
9
+ raise 'abstract method'
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,53 @@
1
+ require_relative 'base_formatter'
2
+
3
+ module RubyLsp
4
+ module Ree
5
+ class MissingErrorContractsFormatter < BaseFormatter
6
+ def call(source, _uri)
7
+ parsed_doc = RubyLsp::Ree::ParsedDocumentBuilder.build_from_source(source)
8
+ return source if !parsed_doc || !parsed_doc.class_node
9
+
10
+ parsed_doc.parse_error_definitions
11
+ parsed_doc.parse_instance_methods
12
+
13
+ parsed_doc.doc_instance_methods.select(&:has_contract?).each do |doc_instance_method|
14
+ doc_instance_method.parse_nested_local_methods(parsed_doc.doc_instance_methods)
15
+
16
+ raised_errors = doc_instance_method.raised_errors_nested
17
+ throws_errors = doc_instance_method.throws_errors
18
+
19
+ missed_errors = raised_errors - throws_errors
20
+ source = add_missed_errors(source, doc_instance_method, missed_errors)
21
+ end
22
+
23
+ source
24
+ end
25
+
26
+ private
27
+
28
+ def add_missed_errors(source, doc_instance_method, missed_errors)
29
+ return source if missed_errors.size == 0
30
+
31
+ source_lines = source.lines
32
+
33
+ if doc_instance_method.has_throw_section?
34
+ position = doc_instance_method.throw_arguments_end_position
35
+ line = doc_instance_method.throw_arguments_end_line
36
+
37
+ if source_lines[line].strip.end_with?(")")
38
+ source_lines[line] = source_lines[line][0..position] + ", #{missed_errors.join(', ')})\n"
39
+ else
40
+ source_lines[line] = source_lines[line][0..position] + ", #{missed_errors.join(', ')}\n"
41
+ end
42
+ else
43
+ position = doc_instance_method.contract_node_end_position
44
+ line = doc_instance_method.contract_node_end_line
45
+
46
+ source_lines[line] = source_lines[line][0..position] + ".throws(#{missed_errors.join(', ')})\n"
47
+ end
48
+
49
+ source_lines.join
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,54 @@
1
+ require_relative 'base_formatter'
2
+
3
+ module RubyLsp
4
+ module Ree
5
+ class MissingErrorDefinitionsFormatter < BaseFormatter
6
+ include RubyLsp::Ree::ReeLspUtils
7
+
8
+ def call(source, _uri)
9
+ parsed_doc = RubyLsp::Ree::ParsedDocumentBuilder.build_from_source(source)
10
+ return source if !parsed_doc || !parsed_doc.class_node
11
+
12
+ parsed_doc.parse_error_definitions
13
+ parsed_doc.parse_instance_methods
14
+ parsed_doc.parse_links
15
+
16
+ existing_errors = parsed_doc.error_definition_names + parsed_doc.imported_constants
17
+
18
+ missed_errors = []
19
+ parsed_doc.doc_instance_methods.each do |doc_instance_method|
20
+ doc_instance_method.parse_nested_local_methods(parsed_doc.doc_instance_methods)
21
+
22
+ raised_errors = doc_instance_method.raised_errors_nested
23
+
24
+ missed_errors += raised_errors - existing_errors
25
+ end
26
+
27
+ missed_errors = missed_errors.uniq.reject{ Object.const_defined?(_1) }
28
+
29
+ add_missed_error_definitions(source, parsed_doc, missed_errors.uniq)
30
+ end
31
+
32
+ private
33
+
34
+ def add_missed_error_definitions(source, parsed_doc, missed_errors)
35
+ return source if missed_errors.size == 0
36
+
37
+ source_lines = source.lines
38
+
39
+ if parsed_doc.error_definitions.size > 0
40
+ change_line = parsed_doc.error_definitions.map{ _1.location.start_line }.max - 1
41
+ else
42
+ change_line = parsed_doc.links_container_node.location.end_line - 1
43
+ source_lines[change_line] += "\n"
44
+ end
45
+
46
+ missed_errors.each do |err|
47
+ source_lines[change_line] += "\s\s#{err} = invalid_param_error(:#{underscore(err)})\n"
48
+ end
49
+
50
+ source_lines.join
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,84 @@
1
+ require_relative 'base_formatter'
2
+
3
+ module RubyLsp
4
+ module Ree
5
+ class MissingErrorLocalesFormatter < BaseFormatter
6
+ include RubyLsp::Ree::ReeLspUtils
7
+ include RubyLsp::Ree::ReeLocaleUtils
8
+
9
+ MISSING_LOCALE_PLACEHOLDER = '_MISSING_LOCALE_'
10
+
11
+ def call(source, uri)
12
+ parsed_doc = RubyLsp::Ree::ParsedDocumentBuilder.build_from_source(source)
13
+
14
+ locales_folder = package_locales_folder_path(URI.parse(uri.to_s).path)
15
+ return source if !locales_folder || !File.directory?(locales_folder)
16
+
17
+ result = []
18
+ key_paths = []
19
+ parsed_doc.parse_error_definitions
20
+ parsed_doc.error_definitions.each do |error_definition|
21
+ key_path = if error_definition.value.arguments.arguments.size > 1
22
+ error_definition.value.arguments.arguments[1].unescaped
23
+ else
24
+ mod = underscore(parsed_doc.module_name)
25
+ "#{mod}.errors.#{error_definition.value.arguments.arguments[0].unescaped}"
26
+ end
27
+
28
+ key_paths << key_path
29
+ end
30
+
31
+ Dir.glob(File.join(locales_folder, '**/*.yml')).each do |locale_file|
32
+ key_paths.each do |key_path|
33
+ value = find_locale_value(locale_file, key_path)
34
+ unless value
35
+ loc_key = File.basename(locale_file, '.yml')
36
+
37
+ add_locale_placeholder(locale_file, key_path)
38
+ end
39
+ end
40
+ end
41
+
42
+ source
43
+ end
44
+
45
+ private
46
+
47
+ def add_locale_placeholder(file_path, key_path)
48
+ loc_key = File.basename(file_path, '.yml')
49
+ key_parts = [loc_key] + key_path.split('.')
50
+
51
+ last_found_index = 0
52
+ last_found_key = nil
53
+ current_node = YamlFileParser.parse_with_key_coordinates(file_path)
54
+
55
+ key_parts.each_with_index do |key_part, index|
56
+ found_key, next_node = YamlFileParser.find_key_in_node(current_node, key_part)
57
+
58
+ break unless found_key
59
+
60
+ current_node = next_node
61
+ last_found_index = index
62
+ last_found_key = found_key
63
+ end
64
+
65
+ missed_key_parts = key_parts[last_found_index+1..-1]
66
+
67
+ identation = last_found_key.column
68
+ adding_string = ''
69
+ missed_key_parts.each_with_index do |key_part, index|
70
+ identation += 2
71
+ if index == missed_key_parts.size - 1
72
+ adding_string += "\s" * identation + "#{key_part}: #{MISSING_LOCALE_PLACEHOLDER}\n"
73
+ else
74
+ adding_string += "\s" * identation + "#{key_part}:\n"
75
+ end
76
+ end
77
+
78
+ lines = File.read(file_path).lines
79
+ lines[last_found_key.line] += adding_string
80
+ File.write(file_path, lines.join)
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,51 @@
1
+ require_relative 'base_formatter'
2
+
3
+ module RubyLsp
4
+ module Ree
5
+ class SortLinksFormatter < BaseFormatter
6
+ def call(source, _uri)
7
+ parsed_doc = RubyLsp::Ree::ParsedDocumentBuilder.build_from_source(source)
8
+ return source if !parsed_doc || !parsed_doc.link_nodes&.any?
9
+
10
+ if parsed_doc.link_nodes.any?{ _1.location.start_line != _1.location.end_line }
11
+ $stderr.puts("multiline link definitions, don't sort")
12
+ return source
13
+ end
14
+
15
+ # sort link nodes
16
+ sorted_link_nodes = parsed_doc.link_nodes.sort{ |a, b|
17
+ a_name = a.node.arguments.arguments.first
18
+ b_name = b.node.arguments.arguments.first
19
+
20
+ if a_name.is_a?(Prism::SymbolNode) && !b_name.is_a?(Prism::SymbolNode)
21
+ -1
22
+ elsif b_name.is_a?(Prism::SymbolNode) && !a_name.is_a?(Prism::SymbolNode)
23
+ 1
24
+ else
25
+ a_name.unescaped <=> b_name.unescaped
26
+ end
27
+ }
28
+
29
+ # check if no re-order
30
+ if parsed_doc.link_nodes.map{ _1.node.arguments.arguments.first.unescaped } == sorted_link_nodes.map{ _1.node.arguments.arguments.first.unescaped }
31
+ return source
32
+ end
33
+
34
+ # insert nodes to source
35
+ link_lines = parsed_doc.link_nodes.map{ _1.location.start_line }
36
+
37
+ source_lines = source.lines
38
+
39
+ sorted_lines = sorted_link_nodes.map do |sorted_link|
40
+ source_lines[sorted_link.location.start_line - 1]
41
+ end
42
+
43
+ link_lines.each_with_index do |link_line, index|
44
+ source_lines[link_line - 1] = sorted_lines[index]
45
+ end
46
+
47
+ source_lines.join()
48
+ end
49
+ end
50
+ end
51
+ end
@@ -2,17 +2,20 @@ require_relative "../utils/ree_lsp_utils"
2
2
  require_relative "../ree_object_finder"
3
3
  require_relative "../parsing/parsed_link_node"
4
4
  require_relative "../parsing/parsed_document_builder"
5
+ require_relative "../utils/ree_locale_utils"
5
6
 
6
7
  module RubyLsp
7
8
  module Ree
8
9
  class DefinitionHandler
9
10
  include Requests::Support::Common
10
11
  include RubyLsp::Ree::ReeLspUtils
12
+ include RubyLsp::Ree::ReeLocaleUtils
11
13
 
12
14
  def initialize(index, uri, node_context)
13
15
  @index = index
14
16
  @uri = uri
15
17
  @node_context = node_context
18
+ @root_node = @node_context.instance_variable_get(:@nesting_nodes).first
16
19
  @finder = ReeObjectFinder.new(@index)
17
20
  end
18
21
 
@@ -71,7 +74,15 @@ module RubyLsp
71
74
  message = node.message
72
75
  result = []
73
76
 
74
- definition_item = ReeObjectFinder.new(@index).find_object(message)
77
+ parsed_doc = RubyLsp::Ree::ParsedDocumentBuilder.build_from_ast(@root_node, @uri)
78
+ link_node = parsed_doc.find_link_node(message)
79
+
80
+ definition_item = if link_node
81
+ @finder.find_object_for_package(message, link_node.link_package_name)
82
+ else
83
+ @finder.find_object(message)
84
+ end
85
+
75
86
  return [] unless definition_item
76
87
 
77
88
  definition_uri = definition_item.uri.to_s
@@ -127,6 +138,59 @@ module RubyLsp
127
138
  ),
128
139
  )
129
140
  end
141
+
142
+ def get_error_locales_definition_items(node)
143
+ locales_folder = package_locales_folder_path(@uri.path)
144
+
145
+ return [] unless File.directory?(locales_folder)
146
+
147
+ result = []
148
+ key_path = node.unescaped
149
+
150
+ Dir.glob(File.join(locales_folder, '**/*.yml')).each do |locale_file|
151
+ location = find_locale_key_location(locale_file, key_path)
152
+
153
+ result << Interface::Location.new(
154
+ uri: locale_file,
155
+ range: Interface::Range.new(
156
+ start: Interface::Position.new(line: location.line, character: location.column),
157
+ end: Interface::Position.new(line: location.line, character: location.column),
158
+ ),
159
+ )
160
+ end
161
+
162
+ result
163
+ end
164
+
165
+ def get_error_code_definition_items(node)
166
+ locales_folder = package_locales_folder_path(@uri.path)
167
+ return [] unless File.directory?(locales_folder)
168
+
169
+ result = []
170
+
171
+ key_path = if @node_context.parent.arguments.arguments.size > 1
172
+ @node_context.parent.arguments.arguments[1].unescaped
173
+ else
174
+ parsed_doc = RubyLsp::Ree::ParsedDocumentBuilder.build_from_ast(@root_node, @uri)
175
+
176
+ mod = underscore(parsed_doc.module_name)
177
+ "#{mod}.errors.#{node.unescaped}"
178
+ end
179
+
180
+ Dir.glob(File.join(locales_folder, '**/*.yml')).each do |locale_file|
181
+ location = find_locale_key_location(locale_file, key_path)
182
+
183
+ result << Interface::Location.new(
184
+ uri: locale_file,
185
+ range: Interface::Range.new(
186
+ start: Interface::Position.new(line: location.line, character: location.column),
187
+ end: Interface::Position.new(line: location.line, character: location.column),
188
+ ),
189
+ )
190
+ end
191
+
192
+ result
193
+ end
130
194
  end
131
195
  end
132
196
  end
@@ -2,17 +2,22 @@ require_relative "../ree_object_finder"
2
2
  require_relative "../parsing/parsed_document_builder"
3
3
  require_relative "../parsing/parsed_link_node"
4
4
  require_relative "../utils/ree_lsp_utils"
5
+ require_relative "../utils/ree_locale_utils"
5
6
 
6
7
  module RubyLsp
7
8
  module Ree
8
9
  class HoverHandler
9
10
  include Requests::Support::Common
10
11
  include RubyLsp::Ree::ReeLspUtils
12
+ include RubyLsp::Ree::ReeLocaleUtils
13
+
14
+ MISSING_LOCALE_PLACEHOLDER = '_MISSING_LOCALE_'
11
15
 
12
16
  def initialize(index, node_context)
13
17
  @index = index
14
18
  @node_context = node_context
15
19
  @finder = ReeObjectFinder.new(@index)
20
+ @root_node = @node_context.instance_variable_get(:@nesting_nodes).first
16
21
  end
17
22
 
18
23
  def get_ree_object_hover_items(node)
@@ -61,6 +66,84 @@ module RubyLsp
61
66
 
62
67
  signature.parameters.map(&:decorated_name).join(', ')
63
68
  end
69
+
70
+ def get_error_locales_hover_items(node)
71
+ parsed_doc = RubyLsp::Ree::ParsedDocumentBuilder.build_from_ast(@root_node, nil)
72
+ uri = get_uri_from_object(parsed_doc)
73
+
74
+ locales_folder = package_locales_folder_path(uri.path)
75
+ return [] unless File.directory?(locales_folder)
76
+
77
+ result = []
78
+ key_path = node.unescaped
79
+
80
+ documentation = ''
81
+
82
+ Dir.glob(File.join(locales_folder, '**/*.yml')).each do |locale_file|
83
+ value = find_locale_value(locale_file, key_path)
84
+ loc_key = File.basename(locale_file, '.yml')
85
+
86
+ if value
87
+ if value == MISSING_LOCALE_PLACEHOLDER
88
+ value_location = find_locale_key_location(locale_file, key_path)
89
+ file_uri = "#{locale_file}" # TODO add line to uri :#{value_location.line+1}:#{value_location.column}"
90
+ documentation += "#{loc_key}: [#{value}](#{file_uri})\n\n"
91
+ else
92
+ documentation += "#{loc_key}: #{value}\n\n"
93
+ end
94
+ else
95
+ documentation += "#{loc_key}: [MISSING TRANSLATION](#{locale_file})\n\n"
96
+ end
97
+ end
98
+
99
+ [documentation]
100
+ end
101
+
102
+ def get_error_code_hover_items(node)
103
+ parsed_doc = RubyLsp::Ree::ParsedDocumentBuilder.build_from_ast(@root_node, nil)
104
+ uri = get_uri_from_object(parsed_doc)
105
+
106
+ locales_folder = package_locales_folder_path(uri.path)
107
+ return [] unless File.directory?(locales_folder)
108
+
109
+ result = []
110
+
111
+ key_path = if @node_context.parent.arguments.arguments.size > 1
112
+ @node_context.parent.arguments.arguments[1].unescaped
113
+ else
114
+ mod = underscore(parsed_doc.module_name)
115
+ "#{mod}.errors.#{node.unescaped}"
116
+ end
117
+
118
+ documentation = ''
119
+
120
+ Dir.glob(File.join(locales_folder, '**/*.yml')).each do |locale_file|
121
+ value = find_locale_value(locale_file, key_path)
122
+
123
+ loc_key = File.basename(locale_file, '.yml')
124
+
125
+ if value
126
+ if value == MISSING_LOCALE_PLACEHOLDER
127
+ value_location = find_locale_key_location(locale_file, key_path)
128
+ file_uri = "#{locale_file}" # TODO add line to uri :#{value_location.line+1}:#{value_location.column}"
129
+ documentation += "#{loc_key}: [#{value}](#{file_uri})\n\n"
130
+ else
131
+ documentation += "#{loc_key}: #{value}\n\n"
132
+ end
133
+ else
134
+ documentation += "#{loc_key}: [MISSING TRANSLATION](#{locale_file})\n\n"
135
+ end
136
+ end
137
+
138
+ [documentation]
139
+ end
140
+
141
+ def get_uri_from_object(parsed_doc)
142
+ obj = parsed_doc.links_container_node_name
143
+
144
+ ree_obj = @finder.find_object(obj)
145
+ ree_obj.uri
146
+ end
64
147
  end
65
148
  end
66
149
  end
@@ -1,4 +1,5 @@
1
1
  require_relative "../handlers/definition_handler"
2
+ require_relative "../ree_context"
2
3
 
3
4
  module RubyLsp
4
5
  module Ree
@@ -8,6 +9,7 @@ module RubyLsp
8
9
  def initialize(response_builder, node_context, index, dispatcher, uri)
9
10
  @response_builder = response_builder
10
11
  @handler = RubyLsp::Ree::DefinitionHandler.new(index, uri, node_context)
12
+ @ree_context = RubyLsp::Ree::ReeContext.new(node_context)
11
13
 
12
14
  dispatcher.register(
13
15
  self,
@@ -40,14 +42,24 @@ module RubyLsp
40
42
  end
41
43
 
42
44
  def on_symbol_node_enter(node)
43
- definition_items = @handler.get_linked_object_definition_items(node)
45
+ definition_items = if @ree_context.is_error_definition?
46
+ @handler.get_error_code_definition_items(node)
47
+ else
48
+ @handler.get_linked_object_definition_items(node)
49
+ end
50
+
44
51
  put_items_into_response(definition_items)
45
52
  rescue => e
46
53
  $stderr.puts("error in definition listener(on_symbol_node_enter): #{e.message} : #{e.backtrace.first}")
47
54
  end
48
55
 
49
56
  def on_string_node_enter(node)
50
- definition_items = @handler.get_linked_filepath_definition_items(node)
57
+ definition_items = if @ree_context.is_error_definition?
58
+ @handler.get_error_locales_definition_items(node)
59
+ else
60
+ @handler.get_linked_filepath_definition_items(node)
61
+ end
62
+
51
63
  put_items_into_response(definition_items)
52
64
  rescue => e
53
65
  $stderr.puts("error in definition listener(on_string_node_enter): #{e.message} : #{e.backtrace.first}")
@@ -8,11 +8,13 @@ module RubyLsp
8
8
  def initialize(response_builder, node_context, index, dispatcher)
9
9
  @response_builder = response_builder
10
10
  @handler = RubyLsp::Ree::HoverHandler.new(index, node_context)
11
+ @ree_context = RubyLsp::Ree::ReeContext.new(node_context)
11
12
 
12
13
  dispatcher.register(
13
14
  self,
14
15
  :on_call_node_enter,
15
16
  :on_symbol_node_enter,
17
+ :on_string_node_enter,
16
18
  :on_constant_read_node_enter
17
19
  )
18
20
  end
@@ -36,8 +38,23 @@ module RubyLsp
36
38
  $stderr.puts("error in hover listener(on_call_node_enter): #{e.message} : #{e.backtrace.first}")
37
39
  end
38
40
 
41
+ def on_string_node_enter(node)
42
+ return unless @ree_context.is_error_definition?
43
+
44
+ hover_items = @handler.get_error_locales_hover_items(node)
45
+
46
+ put_items_into_response(hover_items)
47
+ rescue => e
48
+ $stderr.puts("error in hover listener(on_string_node_enter): #{e.message} : #{e.backtrace.first}")
49
+ end
50
+
39
51
  def on_symbol_node_enter(node)
40
- hover_items = @handler.get_linked_object_hover_items(node)
52
+ hover_items = if @ree_context.is_error_definition?
53
+ @handler.get_error_code_hover_items(node)
54
+ else
55
+ @handler.get_linked_object_hover_items(node)
56
+ end
57
+
41
58
  put_items_into_response(hover_items)
42
59
  rescue => e
43
60
  $stderr.puts("error in hover listener(on_symbol_node_enter): #{e.message} : #{e.backtrace.first}")
@@ -1,4 +1,5 @@
1
1
  require_relative 'parsed_link_node'
2
+ require_relative 'parsed_method_node'
2
3
  require 'ostruct'
3
4
 
4
5
  class RubyLsp::Ree::ParsedDocument
@@ -6,9 +7,26 @@ class RubyLsp::Ree::ParsedDocument
6
7
 
7
8
  LINK_DSL_MODULE = 'Ree::LinkDSL'
8
9
 
10
+ ERROR_DEFINITION_NAMES = [
11
+ :auth_error,
12
+ :build_error,
13
+ :conflict_error,
14
+ :invalid_param_error,
15
+ :not_found_error,
16
+ :payment_required_error,
17
+ :permission_error,
18
+ :validation_error
19
+ ]
20
+
21
+ CONTRACT_CALL_NODE_NAMES = [
22
+ :contract,
23
+ :throws
24
+ ]
25
+
9
26
  attr_reader :ast, :package_name, :class_node, :fn_node, :class_includes,
10
27
  :link_nodes, :values, :action_node, :dao_node, :filters,
11
- :bean_node, :bean_methods, :mapper_node, :links_container_block_node, :aggregate_node
28
+ :bean_node, :bean_methods, :mapper_node, :links_container_block_node, :aggregate_node,
29
+ :error_definitions, :error_definition_names, :doc_instance_methods
12
30
 
13
31
  def initialize(ast)
14
32
  @ast = ast
@@ -161,6 +179,25 @@ class RubyLsp::Ree::ParsedDocument
161
179
  .map{ OpenStruct.new(name: node_name(_1).to_s, signatures: parse_signatures_from_params(_1.parameters)) }
162
180
  end
163
181
 
182
+ def parse_instance_methods
183
+ @doc_instance_methods = []
184
+
185
+ current_contract_node = nil
186
+ class_node.body.body.each do |node|
187
+ if node.is_a?(Prism::CallNode) && CONTRACT_CALL_NODE_NAMES.include?(node_name(node))
188
+ current_contract_node = node
189
+ else
190
+ if node.is_a?(Prism::DefNode)
191
+ @doc_instance_methods << RubyLsp::Ree::ParsedMethodNode.new(node, current_contract_node)
192
+ end
193
+
194
+ current_contract_node = nil
195
+ end
196
+ end
197
+
198
+ @doc_instance_methods
199
+ end
200
+
164
201
  def parse_filter_signature(filter_node)
165
202
  return [] unless filter_node
166
203
 
@@ -175,10 +212,24 @@ class RubyLsp::Ree::ParsedDocument
175
212
  [RubyIndexer::Entry::Signature.new(signature_params)]
176
213
  end
177
214
 
215
+ def parse_error_definitions
216
+ return unless class_node
217
+
218
+ @error_definitions = class_node.body.body
219
+ .select{ _1.is_a?(Prism::ConstantWriteNode) }
220
+ .select{ ERROR_DEFINITION_NAMES.include?(node_name(_1.value)) }
221
+
222
+ @error_definition_names = @error_definitions.map(&:name)
223
+ end
224
+
178
225
  def class_name
179
226
  class_node.constant_path.name.to_s
180
227
  end
181
228
 
229
+ def module_name
230
+ class_node.constant_path&.parent&.name.to_s
231
+ end
232
+
182
233
  def full_class_name
183
234
  name_parts = [class_node.constant_path&.parent&.name, class_node.constant_path.name]
184
235
  name_parts.compact.map(&:to_s).join('::')
@@ -193,4 +244,8 @@ class RubyLsp::Ree::ParsedDocument
193
244
 
194
245
  node.name
195
246
  end
247
+
248
+ def imported_constants
249
+ @link_nodes.map(&:imports).flatten.map(&:to_sym)
250
+ end
196
251
  end
@@ -38,11 +38,11 @@ class RubyLsp::Ree::ParsedDocumentBuilder
38
38
  when :bean
39
39
  build_bean_document(ast)
40
40
  else
41
- build_detected_doument_type(ast)
41
+ build_detected_document_type(ast)
42
42
  end
43
43
  end
44
44
 
45
- def self.build_detected_doument_type(ast)
45
+ def self.build_detected_document_type(ast)
46
46
  if has_root_class?(ast)
47
47
  build_regular_document(ast)
48
48
  elsif has_root_rspec_call?(ast)
@@ -53,7 +53,7 @@ class RubyLsp::Ree::ParsedDocumentBuilder
53
53
  end
54
54
 
55
55
  def self.has_root_class?(ast)
56
- ast.statements.body.detect{ |node| node.is_a?(Prism::ClassNode) }
56
+ !!ast.statements.body.detect{ |node| node.is_a?(Prism::ClassNode) }
57
57
  end
58
58
 
59
59
  def self.has_root_rspec_call?(ast)
@@ -0,0 +1,115 @@
1
+ require 'prism'
2
+
3
+ class RubyLsp::Ree::ParsedMethodNode
4
+ attr_reader :method_node, :contract_node
5
+
6
+ def initialize(method_node, contract_node)
7
+ @method_node = method_node
8
+ @contract_node = contract_node
9
+ end
10
+
11
+ def name
12
+ @method_node.name
13
+ end
14
+
15
+ def has_contract?
16
+ !!@contract_node
17
+ end
18
+
19
+ def start_line
20
+ @method_node.location.start_line - 1
21
+ end
22
+
23
+ def end_line
24
+ @method_node.location.end_line - 1
25
+ end
26
+
27
+ def raised_errors_nested
28
+ return @raised_errors_nested if @raised_errors_nested
29
+ raised = raised_errors
30
+
31
+ @nested_local_methods.each do |nested_method|
32
+ raised += nested_method.raised_errors_nested
33
+ end
34
+
35
+ @raised_errors_nested = raised
36
+ end
37
+
38
+ def raised_errors
39
+ return @raised_errors if @raised_errors
40
+ return [] unless @method_node.body
41
+
42
+ call_objects = parse_body_call_objects(@method_node.body.body)
43
+ raise_objects = call_objects.select{ _1.name == :raise }
44
+ @raised_errors = raise_objects.map{ parse_raised_class_name(_1) }.compact
45
+ end
46
+
47
+ def parse_raised_class_name(raise_node)
48
+ return unless raise_node.arguments
49
+
50
+ if raise_node.arguments.arguments.first.is_a?(Prism::ConstantReadNode)
51
+ raise_node.arguments.arguments.first.name
52
+ elsif raise_node.arguments.arguments.first.is_a?(Prism::CallNode)
53
+ raise_node.arguments.arguments.first.receiver.name
54
+ else
55
+ nil
56
+ end
57
+ end
58
+
59
+ def throws_errors
60
+ return [] unless has_contract?
61
+ return [] unless has_throw_section?
62
+
63
+ @contract_node.arguments.arguments.map{ _1.name }
64
+ end
65
+
66
+ def has_throw_section?
67
+ @contract_node && @contract_node.name == :throws
68
+ end
69
+
70
+ def throw_arguments_end_position
71
+ @contract_node.arguments.arguments.last.location.end_column - 1
72
+ end
73
+
74
+ def throw_arguments_end_line
75
+ @contract_node.arguments.arguments.last.location.end_line - 1
76
+ end
77
+
78
+ def contract_node_end_position
79
+ @contract_node.location.end_column - 1
80
+ end
81
+
82
+ def contract_node_end_line
83
+ @contract_node.location.end_line - 1
84
+ end
85
+
86
+ def parse_nested_local_methods(local_methods)
87
+ unless @method_node.body
88
+ @nested_local_methods = []
89
+ return
90
+ end
91
+
92
+ local_method_names = local_methods.map(&:name)
93
+ call_nodes = parse_body_call_objects(@method_node.body.body)
94
+ call_node_names = call_nodes.map(&:name)
95
+
96
+ @nested_local_methods = local_methods.select{ call_node_names.include?(_1.name) }
97
+ @nested_local_methods.each{ _1.parse_nested_local_methods(local_methods) }
98
+ end
99
+
100
+ def parse_body_call_objects(node_body)
101
+ call_nodes = []
102
+
103
+ node_body.each do |node|
104
+ if node.is_a?(Prism::CallNode) && !node.receiver
105
+ call_nodes << node
106
+ elsif node.respond_to?(:statements)
107
+ call_nodes += parse_body_call_objects(node.statements.body)
108
+ elsif node.respond_to?(:block) && node.block
109
+ call_nodes += parse_body_call_objects(node.block.body.body)
110
+ end
111
+ end
112
+
113
+ call_nodes
114
+ end
115
+ end
@@ -0,0 +1,28 @@
1
+ require 'prism'
2
+
3
+ module RubyLsp
4
+ module Ree
5
+ class ReeContext
6
+ ERROR_DEFINITION_NAMES = [
7
+ :auth_error,
8
+ :build_error,
9
+ :conflict_error,
10
+ :invalid_param_error,
11
+ :not_found_error,
12
+ :payment_required_error,
13
+ :permission_error,
14
+ :validation_error
15
+ ]
16
+
17
+ def initialize(node_context)
18
+ @node_context = node_context
19
+ end
20
+
21
+ def is_error_definition?
22
+ return false if !@node_context || !@node_context.parent || !@node_context.parent.is_a?(Prism::CallNode)
23
+
24
+ ERROR_DEFINITION_NAMES.include?(@node_context.parent.name)
25
+ end
26
+ end
27
+ end
28
+ end
@@ -1,63 +1,84 @@
1
+ require_relative 'formatters/sort_links_formatter'
2
+ require_relative 'formatters/missing_error_definitions_formatter'
3
+ require_relative 'formatters/missing_error_contracts_formatter'
4
+ require_relative 'formatters/missing_error_locales_formatter'
5
+
1
6
  module RubyLsp
2
7
  module Ree
3
8
  class ReeFormatter
4
9
  include RubyLsp::Requests::Support::Formatter
5
10
  include RubyLsp::Ree::ReeLspUtils
6
-
7
- def initialize
8
- end
11
+ include RubyLsp::Ree::ReeLocaleUtils
9
12
 
10
13
  def run_formatting(uri, document)
11
14
  source = document.source
12
- sort_links(source)
15
+
16
+ formatters = [
17
+ RubyLsp::Ree::SortLinksFormatter,
18
+ RubyLsp::Ree::MissingErrorDefinitionsFormatter,
19
+ RubyLsp::Ree::MissingErrorContractsFormatter,
20
+ RubyLsp::Ree::MissingErrorLocalesFormatter
21
+ ]
22
+
23
+ formatters.reduce(source){ |s, formatter| formatter.call(s, uri) }
13
24
  rescue => e
14
25
  $stderr.puts("error in ree_formatter: #{e.message} : #{e.backtrace.first}")
15
26
  end
16
27
 
17
- private
28
+ def run_diagnostic(uri, document)
29
+ $stderr.puts("ree_formatter_diagnostic")
30
+ detect_missing_error_locales(uri, document)
31
+ rescue => e
32
+ $stderr.puts("error in ree_formatter_diagnostic: #{e.message} : #{e.backtrace.first}")
33
+ end
18
34
 
19
- def sort_links(source)
20
- parsed_doc = RubyLsp::Ree::ParsedDocumentBuilder.build_from_source(source)
21
- return source if !parsed_doc.link_nodes&.any?
35
+ private
22
36
 
23
- if parsed_doc.link_nodes.any?{ _1.location.start_line != _1.location.end_line }
24
- $stderr.puts("multiline link definitions, don't sort")
25
- return source
26
- end
37
+ def detect_missing_error_locales(uri, document)
38
+ parsed_doc = RubyLsp::Ree::ParsedDocumentBuilder.build_from_source(document.source)
27
39
 
28
- # sort link nodes
29
- sorted_link_nodes = parsed_doc.link_nodes.sort{ |a, b|
30
- a_name = a.node.arguments.arguments.first
31
- b_name = b.node.arguments.arguments.first
40
+ locales_folder = package_locales_folder_path(uri.path)
41
+ return [] unless File.directory?(locales_folder)
32
42
 
33
- if a_name.is_a?(Prism::SymbolNode) && !b_name.is_a?(Prism::SymbolNode)
34
- -1
35
- elsif b_name.is_a?(Prism::SymbolNode) && !a_name.is_a?(Prism::SymbolNode)
36
- 1
43
+ result = []
44
+ key_paths = []
45
+ parsed_doc.parse_error_definitions
46
+ parsed_doc.error_definitions.each do |error_definition|
47
+ key_path = if error_definition.value.arguments.arguments.size > 1
48
+ error_definition.value.arguments.arguments[1].unescaped
37
49
  else
38
- a_name.unescaped <=> b_name.unescaped
50
+ mod = underscore(parsed_doc.module_name)
51
+ "#{mod}.errors.#{error_definition.value.arguments.arguments[0].unescaped}"
39
52
  end
40
- }
41
53
 
42
- # check if no re-order
43
- if parsed_doc.link_nodes.map{ _1.node.arguments.arguments.first.unescaped } == sorted_link_nodes.map{ _1.node.arguments.arguments.first.unescaped }
44
- return source
54
+ key_paths << key_path
45
55
  end
46
56
 
47
- # insert nodes to source
48
- link_lines = parsed_doc.link_nodes.map{ _1.location.start_line }
57
+ $stderr.puts("ree_formatter_diagnostic #{key_paths}")
49
58
 
50
- source_lines = source.lines
59
+ Dir.glob(File.join(locales_folder, '**/*.yml')).each do |locale_file|
60
+ key_paths.each do |key_path|
61
+ value = find_locale_value(locale_file, key_path)
62
+ unless value
63
+ loc_key = File.basename(locale_file, '.yml')
51
64
 
52
- sorted_lines = sorted_link_nodes.map do |sorted_link|
53
- source_lines[sorted_link.location.start_line - 1]
54
- end
65
+ $stderr.puts("ree_formatter_diagnostic add diagnostic")
55
66
 
56
- link_lines.each_with_index do |link_line, index|
57
- source_lines[link_line - 1] = sorted_lines[index]
67
+ # TODO correct error range
68
+ result << RubyLsp::Interface::Diagnostic.new(
69
+ message: "Missing locale #{loc_key}: #{key_path}",
70
+ source: "Ree formatter",
71
+ severity: RubyLsp::Constant::DiagnosticSeverity::ERROR,
72
+ range: RubyLsp::Interface::Range.new(
73
+ start: RubyLsp::Interface::Position.new(line: 0, character: 0),
74
+ end: RubyLsp::Interface::Position.new(line: 0, character: 0),
75
+ ),
76
+ )
77
+ end
78
+ end
58
79
  end
59
80
 
60
- source_lines.join()
81
+ result
61
82
  end
62
83
  end
63
84
  end
@@ -1,6 +1,10 @@
1
+ require_relative "utils/ree_lsp_utils"
2
+
1
3
  module RubyLsp
2
4
  module Ree
3
5
  class ReeObjectFinder
6
+ include RubyLsp::Ree::ReeLspUtils
7
+
4
8
  MAX_LIMIT = 1000
5
9
 
6
10
  REE_OBJECT_STRING = 'ree_object'
@@ -42,6 +46,13 @@ module RubyLsp
42
46
  objects_by_name.detect{ _1.comments.to_s.lines.first&.chomp == REE_OBJECT_STRING }
43
47
  end
44
48
 
49
+ def find_object_for_package(name, package_name)
50
+ objects_by_name = @index[name]
51
+ return unless objects_by_name
52
+
53
+ objects_by_name.detect{ _1.comments.to_s.lines.first&.chomp == REE_OBJECT_STRING && package_name_from_uri(_1.uri) == package_name }
54
+ end
55
+
45
56
  def find_objects_by_types(name, types)
46
57
  objects_by_name = @index[name]
47
58
  return [] unless objects_by_name
@@ -0,0 +1,40 @@
1
+ require 'yaml'
2
+ require_relative 'yaml_file_parser'
3
+
4
+ module RubyLsp
5
+ module Ree
6
+ module ReeLocaleUtils
7
+ def package_locales_folder_path(uri)
8
+ uri_parts = uri.to_s.chomp(File.extname(uri.to_s)).split('/')
9
+
10
+ package_folder_index = uri_parts.index('package')
11
+ return unless package_folder_index
12
+
13
+ path_parts = uri_parts.take(package_folder_index+2) + ['locales']
14
+ path_parts.join('/')
15
+ end
16
+
17
+ def find_locale_value(file_path, key_path)
18
+ loc_yaml = YAML.load_file(file_path)
19
+ loc_key = File.basename(file_path, '.yml')
20
+ key_parts = [loc_key] + key_path.split('.')
21
+
22
+ loc_yaml.dig(*key_parts)
23
+ end
24
+
25
+ def find_locale_key_location(file_path, key_path)
26
+ loc_key = File.basename(file_path, '.yml')
27
+
28
+ key_parts = [loc_key] + key_path.split('.')
29
+ parsed_yaml = RubyLsp::Ree::YamlFileParser.parse(file_path)
30
+ key_location = parsed_yaml.dig(*key_parts)
31
+
32
+ if key_location
33
+ OpenStruct.new(line: key_location.line, column: key_location.column)
34
+ else
35
+ OpenStruct.new(line: 0, column: 0)
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -7,7 +7,7 @@ module RubyLsp
7
7
  file_name = file_path + ".rb"
8
8
  Dir[File.join('**', file_name)].first
9
9
  end
10
-
10
+
11
11
  def package_name_from_uri(uri)
12
12
  uri_parts = uri.to_s.split('/')
13
13
 
@@ -170,6 +170,17 @@ module RubyLsp
170
170
  :"(#{names_with_commas})"
171
171
  end
172
172
  end
173
+
174
+ # copied from ree string_utils
175
+ def underscore(camel_cased_word)
176
+ return camel_cased_word unless /[A-Z-]|::/.match?(camel_cased_word)
177
+ word = camel_cased_word.to_s.gsub("::".freeze, "/".freeze)
178
+ word.gsub!(/([A-Z\d]+)([A-Z][a-z])/, '\1_\2'.freeze)
179
+ word.gsub!(/([a-z\d])([A-Z])/, '\1_\2'.freeze)
180
+ word.tr!("-".freeze, "_".freeze)
181
+ word.downcase!
182
+ word
183
+ end
173
184
  end
174
185
  end
175
186
  end
@@ -0,0 +1,53 @@
1
+ require 'psych'
2
+
3
+ module RubyLsp
4
+ module Ree
5
+ class NodeVisitor < Psych::Visitors::ToRuby
6
+ def visit_Psych_Nodes_Scalar(o)
7
+ register(o, OpenStruct.new(value: deserialize(o), line: o.start_line, column: o.start_column))
8
+ end
9
+ end
10
+
11
+ class YamlFileParser
12
+ def self.parse(file_path)
13
+ parser = Psych::Parser.new(Psych::TreeBuilder.new)
14
+ parser.parse(File.read(file_path))
15
+
16
+ parse_result = NodeVisitor.create.accept(parser.handler.root)
17
+ normalize_hash_keys(parse_result.first)
18
+ end
19
+
20
+ def self.parse_with_key_coordinates(file_path)
21
+ parser = Psych::Parser.new(Psych::TreeBuilder.new)
22
+ parser.parse(File.read(file_path))
23
+
24
+ NodeVisitor.create.accept(parser.handler.root).first
25
+ end
26
+
27
+ def self.find_key_in_node(current_node, key)
28
+ matched_key_el = current_node.detect{ _1[0].value == key }
29
+ return [nil, nil] unless matched_key_el
30
+ matched_key_el
31
+ end
32
+
33
+ def self.normalize_hash_keys(res)
34
+ deep_transform_keys_in_object!(res){ |k| k.value }
35
+ end
36
+
37
+ def self.deep_transform_keys_in_object!(object, &block)
38
+ case object
39
+ when Hash
40
+ object.keys.each do |key|
41
+ value = object.delete(key)
42
+ object[yield(key)] = deep_transform_keys_in_object!(value, &block)
43
+ end
44
+ object
45
+ when Array
46
+ object.map! { |e| deep_transform_keys_in_object!(e, &block) }
47
+ else
48
+ object
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module RubyLsp
4
4
  module Ree
5
- VERSION = "0.1.6"
5
+ VERSION = "0.1.8"
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruby-lsp-ree
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.6
4
+ version: 0.1.8
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ruslan Gatiyatov
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2025-03-05 00:00:00.000000000 Z
11
+ date: 2025-03-21 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: A Ruby LSP addon that adds extra editor functionality for Ree applications
14
14
  email:
@@ -26,6 +26,11 @@ files:
26
26
  - README.md
27
27
  - Rakefile
28
28
  - lib/ruby_lsp/ruby_lsp_ree/addon.rb
29
+ - lib/ruby_lsp/ruby_lsp_ree/formatters/base_formatter.rb
30
+ - lib/ruby_lsp/ruby_lsp_ree/formatters/missing_error_contracts_formatter.rb
31
+ - lib/ruby_lsp/ruby_lsp_ree/formatters/missing_error_definitions_formatter.rb
32
+ - lib/ruby_lsp/ruby_lsp_ree/formatters/missing_error_locales_formatter.rb
33
+ - lib/ruby_lsp/ruby_lsp_ree/formatters/sort_links_formatter.rb
29
34
  - lib/ruby_lsp/ruby_lsp_ree/handlers/completion_handler.rb
30
35
  - lib/ruby_lsp/ruby_lsp_ree/handlers/definition_handler.rb
31
36
  - lib/ruby_lsp/ruby_lsp_ree/handlers/hover_handler.rb
@@ -35,13 +40,17 @@ files:
35
40
  - lib/ruby_lsp/ruby_lsp_ree/parsing/parsed_document.rb
36
41
  - lib/ruby_lsp/ruby_lsp_ree/parsing/parsed_document_builder.rb
37
42
  - lib/ruby_lsp/ruby_lsp_ree/parsing/parsed_link_node.rb
43
+ - lib/ruby_lsp/ruby_lsp_ree/parsing/parsed_method_node.rb
38
44
  - lib/ruby_lsp/ruby_lsp_ree/parsing/parsed_rspec_document.rb
45
+ - lib/ruby_lsp/ruby_lsp_ree/ree_context.rb
39
46
  - lib/ruby_lsp/ruby_lsp_ree/ree_formatter.rb
40
47
  - lib/ruby_lsp/ruby_lsp_ree/ree_indexing_enhancement.rb
41
48
  - lib/ruby_lsp/ruby_lsp_ree/ree_object_finder.rb
42
49
  - lib/ruby_lsp/ruby_lsp_ree/ree_rename_handler.rb
43
50
  - lib/ruby_lsp/ruby_lsp_ree/ree_template_applicator.rb
51
+ - lib/ruby_lsp/ruby_lsp_ree/utils/ree_locale_utils.rb
44
52
  - lib/ruby_lsp/ruby_lsp_ree/utils/ree_lsp_utils.rb
53
+ - lib/ruby_lsp/ruby_lsp_ree/utils/yaml_file_parser.rb
45
54
  - lib/ruby_lsp_ree.rb
46
55
  - lib/ruby_lsp_ree/version.rb
47
56
  - ruby-lsp-ree.gemspec