glib-web 4.39.0 → 4.39.2

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.
Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/concerns/glib/auth/policy.rb +13 -0
  3. data/app/controllers/glib/api_docs_controller.rb +145 -0
  4. data/app/helpers/glib/json_ui/abstract_builder.rb +16 -0
  5. data/app/helpers/glib/json_ui/action_builder/dialogs.rb +4 -0
  6. data/app/helpers/glib/json_ui/list_builders.rb +2 -0
  7. data/app/helpers/glib/json_ui/view_builder/fields.rb +15 -0
  8. data/app/helpers/glib/json_ui/view_builder/panels.rb +450 -11
  9. data/app/helpers/glib/json_ui/view_builder.rb +1 -1
  10. data/app/views/glib/api_docs/component.json.jbuilder +215 -0
  11. data/app/views/glib/api_docs/index.json.jbuilder +103 -0
  12. data/app/views/glib/api_docs/show.json.jbuilder +111 -0
  13. data/app/views/json_ui/garage/actions/_dialogs.json.jbuilder +2 -2
  14. data/app/views/json_ui/garage/forms/dynamic_group.json.jbuilder +2 -2
  15. data/app/views/json_ui/garage/forms/file_upload_new.json.jbuilder +1 -1
  16. data/app/views/json_ui/garage/forms/partial_update.json.jbuilder +12 -12
  17. data/app/views/json_ui/garage/forms/selects.json.jbuilder +1 -1
  18. data/app/views/json_ui/garage/forms/show_hide.json.jbuilder +62 -7
  19. data/app/views/json_ui/garage/lists/edit_mode.json.jbuilder +4 -4
  20. data/app/views/json_ui/garage/lists/templating.json.jbuilder +68 -44
  21. data/app/views/json_ui/garage/pages/custom_style_class.json.jbuilder +1 -1
  22. data/app/views/json_ui/garage/pages/nav_buttons.json.jbuilder +31 -13
  23. data/app/views/json_ui/garage/panels/_styled.json.jbuilder +8 -8
  24. data/app/views/json_ui/garage/panels/hover.json.jbuilder +2 -2
  25. data/app/views/json_ui/garage/panels/timeline.json.jbuilder +5 -5
  26. data/app/views/json_ui/garage/panels/ul.json.jbuilder +1 -1
  27. data/app/views/json_ui/garage/tables/bulk_edit.json.jbuilder +1 -1
  28. data/app/views/json_ui/garage/test_page/file_upload_new.json.jbuilder +1 -1
  29. data/app/views/json_ui/garage/test_page/form.json.jbuilder +6 -6
  30. data/app/views/json_ui/garage/test_page/form_dynamic.json.jbuilder +2 -2
  31. data/app/views/json_ui/garage/test_page/logics_set.json.jbuilder +94 -0
  32. data/app/views/json_ui/garage/views/components_replace.json.jbuilder +13 -13
  33. data/app/views/json_ui/garage/views/components_set.json.jbuilder +6 -6
  34. data/app/views/json_ui/garage/views/fields_focus.json.jbuilder +22 -22
  35. data/app/views/json_ui/garage/views/markdowns.json.jbuilder +2 -0
  36. data/config/routes.rb +4 -0
  37. data/lib/glib/doc_generator.rb +386 -0
  38. data/lib/glib/json_crawler/router.rb +45 -24
  39. data/lib/glib/rubocop/cops/json_ui/base_nested_parameter.rb +145 -0
  40. data/lib/glib/rubocop/cops/json_ui/nested_action_parameter.rb +55 -0
  41. data/lib/glib/rubocop/cops/json_ui/nested_block_parameter.rb +51 -0
  42. data/lib/glib/rubocop/cops/multiline_method_call_style.rb +74 -5
  43. data/lib/glib/rubocop/cops/test_name_parentheses.rb +33 -0
  44. data/lib/glib/rubocop.rb +4 -0
  45. data/lib/glib/test/parallel_coverage.rb +38 -0
  46. data/lib/glib/test_helpers.rb +12 -0
  47. data/lib/glib-web.rb +1 -0
  48. data/lib/tasks/db.rake +1 -1
  49. data/lib/tasks/docs.rake +59 -0
  50. metadata +13 -1
@@ -0,0 +1,386 @@
1
+ require 'parser/current'
2
+ require 'yaml'
3
+ require 'fileutils'
4
+ require 'time'
5
+
6
+ module Glib
7
+ class DocGenerator
8
+ def initialize
9
+ @parser = Parser::CurrentRuby
10
+ end
11
+
12
+ # Generate YAML documentation for a single Ruby file
13
+ def generate_for_file(file_path, output_path)
14
+ unless File.exist?(file_path)
15
+ puts "Warning: File not found: #{file_path}"
16
+ return
17
+ end
18
+
19
+ source = File.read(file_path)
20
+ ast = @parser.parse(source)
21
+
22
+ components = extract_components(ast, source)
23
+
24
+ # Generate YAML
25
+ yaml_data = {
26
+ 'meta' => {
27
+ 'source_file' => file_path,
28
+ 'generated_at' => Time.now.iso8601,
29
+ 'generator_version' => '1.0.0'
30
+ },
31
+ 'components' => components
32
+ }
33
+
34
+ # Ensure output directory exists
35
+ FileUtils.mkdir_p(File.dirname(output_path))
36
+
37
+ # Write YAML file
38
+ File.write(output_path, yaml_data.to_yaml)
39
+ end
40
+
41
+ private
42
+
43
+ # Extract component information from AST
44
+ def extract_components(node, source)
45
+ components = {}
46
+
47
+ traverse_ast(node, source) do |class_node, class_name, parent_class, comment|
48
+ next unless class_node.type == :class
49
+
50
+ # Parse YARD comment
51
+ yard_doc = parse_yard_comment(comment)
52
+
53
+ # Extract properties
54
+ properties = extract_properties(class_node, source)
55
+
56
+ # Build component hash
57
+ component_key = underscore(class_name)
58
+ components[component_key] = {
59
+ 'class_name' => class_name,
60
+ 'extends' => parent_class,
61
+ 'description' => yard_doc[:description],
62
+ 'properties' => properties,
63
+ 'examples' => yard_doc[:examples],
64
+ 'references' => yard_doc[:references],
65
+ 'notes' => yard_doc[:notes],
66
+ 'deprecated' => yard_doc[:deprecated]
67
+ }.compact
68
+ end
69
+
70
+ components
71
+ end
72
+
73
+ # Traverse AST and yield class nodes with their comments
74
+ def traverse_ast(node, source, &block)
75
+ return unless node.is_a?(Parser::AST::Node)
76
+
77
+ if node.type == :class
78
+ class_name = extract_class_name(node)
79
+ parent_class = extract_parent_class(node)
80
+ comment = extract_comment(node, source)
81
+
82
+ yield node, class_name, parent_class, comment
83
+ end
84
+
85
+ # Recursively traverse child nodes
86
+ node.children.each do |child|
87
+ traverse_ast(child, source, &block)
88
+ end
89
+ end
90
+
91
+ # Extract class name from class node
92
+ def extract_class_name(class_node)
93
+ const_node = class_node.children[0]
94
+ if const_node.type == :const
95
+ const_node.children[1].to_s
96
+ else
97
+ 'Unknown'
98
+ end
99
+ end
100
+
101
+ # Extract parent class name
102
+ def extract_parent_class(class_node)
103
+ parent_node = class_node.children[1]
104
+ return nil unless parent_node
105
+
106
+ if parent_node.type == :const
107
+ parent_node.children[1].to_s
108
+ else
109
+ nil
110
+ end
111
+ end
112
+
113
+ # Extract comment before a node
114
+ def extract_comment(node, source)
115
+ return '' unless node.location
116
+
117
+ # Get all lines before the node
118
+ lines = source.lines
119
+ node_line = node.location.line - 1
120
+
121
+ # Walk backwards to collect comment lines
122
+ comment_lines = []
123
+ (node_line - 1).downto(0) do |i|
124
+ line = lines[i].strip
125
+ break unless line.start_with?('#') || line.empty?
126
+
127
+ if line.start_with?('#')
128
+ # Remove leading # and whitespace
129
+ comment_lines.unshift(line.sub(/^#\s?/, ''))
130
+ end
131
+ end
132
+
133
+ comment_lines.join("\n")
134
+ end
135
+
136
+ # Parse YARD comment into structured data
137
+ def parse_yard_comment(comment)
138
+ return {} if comment.empty?
139
+
140
+ result = {
141
+ description: '',
142
+ examples: [],
143
+ references: [],
144
+ notes: [],
145
+ deprecated: nil
146
+ }
147
+
148
+ current_section = :description
149
+ current_example = nil
150
+ description_lines = []
151
+
152
+ comment.lines.each do |line|
153
+ line = line.chomp
154
+
155
+ # Check for YARD tags
156
+ if line =~ /^@example\s*(.*)/
157
+ # Save current example if exists
158
+ result[:examples] << current_example if current_example
159
+
160
+ current_example = {
161
+ 'label' => $1.strip,
162
+ 'code' => ''
163
+ }
164
+ current_section = :example
165
+ elsif line =~ /^@see\s+(.*)/
166
+ reference = $1.strip
167
+ # Parse URL and description
168
+ if reference =~ /^(https?:\/\/\S+)\s+(.*)/
169
+ result[:references] << { 'url' => $1, 'description' => $2 }
170
+ elsif reference =~ /^(https?:\/\/\S+)/
171
+ result[:references] << { 'url' => $1 }
172
+ else
173
+ result[:references] << { 'text' => reference }
174
+ end
175
+ current_section = :description
176
+ elsif line =~ /^@note\s+(.*)/
177
+ result[:notes] << $1.strip
178
+ current_section = :description
179
+ elsif line =~ /^@deprecated\s*(.*)/
180
+ result[:deprecated] = $1.strip
181
+ result[:deprecated] = true if result[:deprecated].empty?
182
+ current_section = :description
183
+ else
184
+ # Regular content
185
+ if current_section == :example && current_example
186
+ # Remove leading spaces from example code (preserve relative indentation)
187
+ current_example['code'] += line + "\n"
188
+ elsif current_section == :description
189
+ description_lines << line unless line.strip.empty? && description_lines.empty?
190
+ end
191
+ end
192
+ end
193
+
194
+ # Save last example
195
+ result[:examples] << current_example if current_example
196
+
197
+ # Clean up example code (remove extra leading whitespace)
198
+ result[:examples].each do |example|
199
+ example['code'] = unindent(example['code'])
200
+ end
201
+
202
+ # Join description lines
203
+ result[:description] = description_lines.join("\n").strip
204
+
205
+ # Remove empty arrays/nils
206
+ result.delete(:examples) if result[:examples].empty?
207
+ result.delete(:references) if result[:references].empty?
208
+ result.delete(:notes) if result[:notes].empty?
209
+ result.delete(:deprecated) if result[:deprecated].nil?
210
+
211
+ result
212
+ end
213
+
214
+ # Extract properties from class body
215
+ def extract_properties(class_node, source)
216
+ properties = {}
217
+
218
+ class_body = class_node.children[2]
219
+ return properties unless class_body
220
+
221
+ traverse_class_body(class_body, source) do |method_name, args, comment|
222
+ # These are the DSL property definition methods
223
+ if [:string, :int, :bool, :float, :action, :views, :array, :hash,
224
+ :icon, :color, :length, :date, :date_time, :text, :any,
225
+ :panels_builder, :menu, :singleton_array].include?(method_name)
226
+
227
+ property_name = args.first
228
+ next unless property_name
229
+
230
+ # Parse property comment
231
+ prop_doc = parse_property_comment(comment)
232
+
233
+ # Extract options if present (for hash, singleton_array, etc.)
234
+ options = extract_property_options(args)
235
+
236
+ properties[property_name] = {
237
+ 'type' => method_name.to_s,
238
+ 'description' => prop_doc[:description],
239
+ 'required' => options[:required] || false,
240
+ 'options' => options[:options],
241
+ 'examples' => prop_doc[:examples],
242
+ 'notes' => prop_doc[:notes],
243
+ 'deprecated' => prop_doc[:deprecated]
244
+ }.compact
245
+
246
+ # Remove empty arrays
247
+ properties[property_name].delete('examples') if properties[property_name]['examples']&.empty?
248
+ properties[property_name].delete('notes') if properties[property_name]['notes']&.empty?
249
+ end
250
+ end
251
+
252
+ properties
253
+ end
254
+
255
+ # Traverse class body to find method calls (property definitions)
256
+ def traverse_class_body(node, source, &block)
257
+ return unless node.is_a?(Parser::AST::Node)
258
+
259
+ if node.type == :send
260
+ receiver = node.children[0]
261
+ method_name = node.children[1]
262
+ args = node.children[2..-1].map { |arg| extract_symbol_or_string(arg) }
263
+ comment = extract_comment(node, source)
264
+
265
+ # Only process calls without a receiver (DSL methods)
266
+ yield method_name, args, comment if receiver.nil?
267
+ end
268
+
269
+ node.children.each do |child|
270
+ traverse_class_body(child, source, &block)
271
+ end
272
+ end
273
+
274
+ # Extract symbol or string value from AST node
275
+ def extract_symbol_or_string(node)
276
+ return nil unless node.is_a?(Parser::AST::Node)
277
+
278
+ case node.type
279
+ when :sym
280
+ node.children[0].to_s
281
+ when :str
282
+ node.children[0]
283
+ when :hash
284
+ extract_hash(node)
285
+ else
286
+ nil
287
+ end
288
+ end
289
+
290
+ # Extract hash from AST node
291
+ def extract_hash(node)
292
+ return nil unless node.type == :hash
293
+
294
+ result = {}
295
+ node.children.each do |pair|
296
+ next unless pair.type == :pair
297
+ key = extract_symbol_or_string(pair.children[0])
298
+ value = extract_symbol_or_string(pair.children[1]) || extract_array(pair.children[1])
299
+ result[key] = value if key
300
+ end
301
+ result
302
+ end
303
+
304
+ # Extract array from AST node
305
+ def extract_array(node)
306
+ return nil unless node.is_a?(Parser::AST::Node) && node.type == :array
307
+
308
+ node.children.map { |child| extract_symbol_or_string(child) }.compact
309
+ end
310
+
311
+ # Extract property options from arguments
312
+ def extract_property_options(args)
313
+ options = { required: false }
314
+
315
+ args.each do |arg|
316
+ if arg.is_a?(Hash)
317
+ if arg['required']
318
+ options[:required] = true
319
+ end
320
+ if arg['optional']
321
+ options[:options] = { 'optional' => arg['optional'] }
322
+ end
323
+ end
324
+ end
325
+
326
+ options
327
+ end
328
+
329
+ # Parse property comment
330
+ def parse_property_comment(comment)
331
+ return {} if comment.empty?
332
+
333
+ result = {
334
+ description: '',
335
+ examples: [],
336
+ notes: [],
337
+ deprecated: nil
338
+ }
339
+
340
+ description_lines = []
341
+
342
+ comment.lines.each do |line|
343
+ line = line.chomp
344
+
345
+ if line =~ /^@example\s+(.*)/
346
+ result[:examples] << $1.strip
347
+ elsif line =~ /^@note\s+(.*)/
348
+ result[:notes] << $1.strip
349
+ elsif line =~ /^@deprecated\s*(.*)/
350
+ result[:deprecated] = $1.strip
351
+ result[:deprecated] = true if result[:deprecated].empty?
352
+ else
353
+ description_lines << line unless line.strip.empty? && description_lines.empty?
354
+ end
355
+ end
356
+
357
+ result[:description] = description_lines.join("\n").strip
358
+
359
+ result
360
+ end
361
+
362
+ # Convert CamelCase to snake_case
363
+ def underscore(string)
364
+ string.gsub(/::/, '_')
365
+ .gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
366
+ .gsub(/([a-z\d])([A-Z])/, '\1_\2')
367
+ .tr('-', '_')
368
+ .downcase
369
+ end
370
+
371
+ # Remove common leading whitespace from multi-line strings
372
+ def unindent(text)
373
+ return text if text.empty?
374
+
375
+ lines = text.lines
376
+ # Find minimum indentation (ignoring empty lines)
377
+ min_indent = lines
378
+ .reject { |line| line.strip.empty? }
379
+ .map { |line| line.match(/^(\s*)/)[1].length }
380
+ .min || 0
381
+
382
+ # Remove that amount of indentation from each line
383
+ lines.map { |line| line.strip.empty? ? line : line[min_indent..-1] }.join
384
+ end
385
+ end
386
+ end
@@ -8,20 +8,20 @@ module Glib
8
8
  attr_reader :http_actions
9
9
  attr_accessor :host, :skip_similar_page
10
10
 
11
- def log(action, url, response = nil)
12
- if url.present?
13
- url = remove_params(url, [:__glib_permission_test])
11
+ def log(action, key_data, response = nil)
12
+ # Sometimes `key_data` may not be an actual URL, e.g. in the context of dialogs_alert,
13
+ # it is the alert message.
14
+ if key_data&.start_with?('http://', 'https://')
15
+ key_data = remove_params(key_data, [:__glib_permission_test])
14
16
  end
15
17
 
16
18
  @last_log = [
17
19
  action,
18
20
  response.present? ? response.code : nil,
19
- url
21
+ key_data
20
22
  ].compact.join(
21
23
  ' :: '
22
- )
23
-
24
- # puts @last_log
24
+ )
25
25
 
26
26
  @logger += ' ' * @depth + @last_log + "\n"
27
27
  end
@@ -172,24 +172,27 @@ module Glib
172
172
  crawler_actions.each do |crawler_action|
173
173
  action, url, params = crawler_action
174
174
 
175
- if url.present?
176
- url = add_params(url, __glib_permission_test: true)
177
- end
178
-
179
- params = JSON.parse(params) if params.is_a?(String)
180
- params ||= {}
181
-
182
- case action.to_s.downcase
183
- when 'http/post-v1', 'forms/post'
184
- http.post(url, action, params)
185
- when 'http/patch-v1', 'forms/patch'
186
- http.patch(url, action, params)
187
- when 'http/put-v1', 'forms/put'
188
- http.put(url, action, params)
189
- when 'http/delete-v1'
190
- http.delete(url, action, params)
175
+ # In full mode, wrap each action in a transaction that gets rolled back
176
+ # to ensure database state is reset between each URL check (prevent database contamination)
177
+ if ENV['GLIB_DISABLE_PERMISSION_TEST_SKIP'] == 'true'
178
+ # This solution is important for permissions tests (not as much in the crawler tests),
179
+ # because in permission tests, the user hits every single available URLs with a single purpose
180
+ # of checking the permission of every URL, meaning that one incorrect result (e.g. 403 instead of 200 due to
181
+ # side effect from previous URL requests) cannot be tolerated.
182
+ #
183
+ # On the other hand, crawler tests are expected to cover only one scenario anyway, so
184
+ # having the scenario changed (due to side effects) is fine. We decided it's better not
185
+ # to apply this solution for crawler tests out of performance considerations.
186
+ ActiveRecord::Base.transaction do
187
+ execute_crawler_action(http, action, url, params)
188
+ raise ActiveRecord::Rollback
189
+ end
191
190
  else
192
- http.get(url, action, params)
191
+ # In skip mode, add the permission test parameter
192
+ if url.present?
193
+ url = add_params(url, __glib_permission_test: true)
194
+ end
195
+ execute_crawler_action(http, action, url, params)
193
196
  end
194
197
  end
195
198
  end
@@ -224,6 +227,24 @@ module Glib
224
227
  end
225
228
 
226
229
  private
230
+ def execute_crawler_action(http, action, url, params)
231
+ params = JSON.parse(params) if params.is_a?(String)
232
+ params ||= {}
233
+
234
+ case action.to_s.downcase
235
+ when 'http/post-v1', 'forms/post'
236
+ http.post(url, action, params)
237
+ when 'http/patch-v1', 'forms/patch'
238
+ http.patch(url, action, params)
239
+ when 'http/put-v1', 'forms/put'
240
+ http.put(url, action, params)
241
+ when 'http/delete-v1'
242
+ http.delete(url, action, params)
243
+ else
244
+ http.get(url, action, params)
245
+ end
246
+ end
247
+
227
248
  def add_params(url, new_params)
228
249
  uri = URI(url)
229
250
  existing = URI.decode_www_form(uri.query || '')
@@ -0,0 +1,145 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Glib
6
+ module JsonUi
7
+ # Base class for checking nested block parameter violations in JsonUi templates.
8
+ # Handles both parameter shadowing and incorrect usage of outer variables.
9
+ class BaseNestedParameter < Base
10
+ extend AutoCorrector
11
+
12
+ MSG_OUTER_VAR = 'Use the immediate block parameter `%<immediate>s` instead of outer variable `%<outer>s`.'
13
+
14
+ def on_block(node)
15
+ check_nested_block(node)
16
+ end
17
+
18
+ alias on_numblock on_block
19
+
20
+ private
21
+
22
+ def check_nested_block(node)
23
+ block_param = extract_block_param(node)
24
+ return unless block_param
25
+ return unless relevant_block?(node)
26
+
27
+ parent_blocks = find_parent_relevant_blocks(node)
28
+ return if parent_blocks.empty?
29
+
30
+ parent_params = parent_blocks.filter_map { |b| extract_block_param(b) }
31
+
32
+ # Check for parameter shadowing
33
+ check_shadowing(node, block_param, parent_params)
34
+
35
+ # Check for incorrect usage of outer variables
36
+ check_send_nodes_in_body(node.body, block_param, parent_params)
37
+ end
38
+
39
+ def check_shadowing(node, block_param, parent_params)
40
+ parent_params.each do |parent_param|
41
+ next unless block_param == parent_param
42
+
43
+ param_node = node.arguments.children.first
44
+ suggested_name = suggest_param_name(parent_param)
45
+
46
+ add_offense(
47
+ param_node,
48
+ message: shadowing_message(parent_param, suggested_name)
49
+ ) do |corrector|
50
+ corrector.replace(param_node, suggested_name)
51
+ replace_param_usages(corrector, node.body, parent_param, suggested_name)
52
+ end
53
+ end
54
+ end
55
+
56
+ def check_send_nodes_in_body(body_node, current_param, parent_params)
57
+ return unless body_node
58
+
59
+ body_node.each_child_node do |child|
60
+ if child.send_type? || child.csend_type?
61
+ check_send_node(child, current_param, parent_params)
62
+ elsif !child.block_type? && !child.numblock_type?
63
+ # Recurse into non-block nodes
64
+ check_send_nodes_in_body(child, current_param, parent_params)
65
+ end
66
+ end
67
+ end
68
+
69
+ def check_send_node(send_node, current_param, parent_params)
70
+ receiver = send_node.receiver
71
+ return unless receiver&.lvar_type?
72
+
73
+ receiver_name = receiver.children.first
74
+
75
+ parent_params.each do |parent_param|
76
+ next unless receiver_name == parent_param.to_sym
77
+
78
+ add_offense(
79
+ receiver,
80
+ message: format(MSG_OUTER_VAR, immediate: current_param, outer: parent_param)
81
+ ) do |corrector|
82
+ corrector.replace(receiver, current_param.to_s)
83
+ end
84
+ break
85
+ end
86
+ end
87
+
88
+ def extract_block_param(node)
89
+ return nil unless node.block_type? || node.numblock_type?
90
+ return nil if node.numblock_type? # Numbered blocks not supported
91
+
92
+ args = node.arguments
93
+ return nil if args.children.empty?
94
+
95
+ args.children.first.children.first
96
+ end
97
+
98
+ def find_parent_relevant_blocks(node)
99
+ blocks = []
100
+ current = node.parent
101
+
102
+ while current
103
+ if (current.block_type? || current.numblock_type?) && current != node
104
+ blocks << current if relevant_block?(current)
105
+ end
106
+ current = current.parent
107
+ end
108
+
109
+ blocks
110
+ end
111
+
112
+ def lambda_block?(block_node)
113
+ block_node.block_type? && block_node.send_node.method_name == :lambda
114
+ end
115
+
116
+ def hash_pair_parent(block_node)
117
+ current = block_node.parent
118
+ current if current&.pair_type?
119
+ end
120
+
121
+ def replace_param_usages(corrector, body_node, old_name, new_name)
122
+ return unless body_node
123
+
124
+ body_node.each_descendant(:lvar) do |lvar_node|
125
+ corrector.replace(lvar_node, new_name) if lvar_node.children.first == old_name.to_sym
126
+ end
127
+ end
128
+
129
+ def suggest_param_name(parent_param)
130
+ parent_param.to_s.start_with?('sub') ? "#{parent_param}2" : "sub#{parent_param}"
131
+ end
132
+
133
+ def shadowing_message(parent_param, suggested_name)
134
+ "Avoid shadowing the outer parameter `#{parent_param}`. Use a different name like `#{suggested_name}`."
135
+ end
136
+
137
+ # To be implemented by subclasses
138
+ def relevant_block?(_block_node)
139
+ raise NotImplementedError, "#{self.class} must implement #relevant_block?"
140
+ end
141
+ end
142
+ end
143
+ end
144
+ end
145
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base_nested_parameter'
4
+
5
+ module RuboCop
6
+ module Cop
7
+ module Glib
8
+ module JsonUi
9
+ # Enforces using the immediate block parameter instead of outer block variables
10
+ # in nested action blocks (onClick, onClose, onChange, etc.) in JsonUi templates.
11
+ #
12
+ # @example
13
+ # # bad - parameter shadowing
14
+ # button onClick: ->(action) do
15
+ # action.dialogs_alert onClose: ->(action) do # shadows outer 'action'
16
+ # action.snackbars_alert message: 'Closed'
17
+ # end
18
+ # end
19
+ #
20
+ # # good
21
+ # button onClick: ->(action) do
22
+ # action.dialogs_alert onClose: ->(subaction) do
23
+ # subaction.snackbars_alert message: 'Closed'
24
+ # end
25
+ # end
26
+ #
27
+ # # bad - using outer variable
28
+ # button onClick: ->(action) do
29
+ # action.dialogs_alert onClose: ->(subaction) do
30
+ # action.forms_submit # using 'action' instead of 'subaction'
31
+ # end
32
+ # end
33
+ #
34
+ # # good
35
+ # button onClick: ->(action) do
36
+ # action.dialogs_alert onClose: ->(subaction) do
37
+ # subaction.forms_submit
38
+ # end
39
+ # end
40
+ class NestedActionParameter < BaseNestedParameter
41
+ private
42
+
43
+ def relevant_block?(block_node)
44
+ return false unless lambda_block?(block_node)
45
+
46
+ pair = hash_pair_parent(block_node)
47
+ return false unless pair
48
+
49
+ pair.key.sym_type? && pair.key.value.to_s.start_with?('on')
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end