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.
- checksums.yaml +4 -4
- data/app/controllers/concerns/glib/auth/policy.rb +13 -0
- data/app/controllers/glib/api_docs_controller.rb +145 -0
- data/app/helpers/glib/json_ui/abstract_builder.rb +16 -0
- data/app/helpers/glib/json_ui/action_builder/dialogs.rb +4 -0
- data/app/helpers/glib/json_ui/list_builders.rb +2 -0
- data/app/helpers/glib/json_ui/view_builder/fields.rb +15 -0
- data/app/helpers/glib/json_ui/view_builder/panels.rb +450 -11
- data/app/helpers/glib/json_ui/view_builder.rb +1 -1
- data/app/views/glib/api_docs/component.json.jbuilder +215 -0
- data/app/views/glib/api_docs/index.json.jbuilder +103 -0
- data/app/views/glib/api_docs/show.json.jbuilder +111 -0
- data/app/views/json_ui/garage/actions/_dialogs.json.jbuilder +2 -2
- data/app/views/json_ui/garage/forms/dynamic_group.json.jbuilder +2 -2
- data/app/views/json_ui/garage/forms/file_upload_new.json.jbuilder +1 -1
- data/app/views/json_ui/garage/forms/partial_update.json.jbuilder +12 -12
- data/app/views/json_ui/garage/forms/selects.json.jbuilder +1 -1
- data/app/views/json_ui/garage/forms/show_hide.json.jbuilder +62 -7
- data/app/views/json_ui/garage/lists/edit_mode.json.jbuilder +4 -4
- data/app/views/json_ui/garage/lists/templating.json.jbuilder +68 -44
- data/app/views/json_ui/garage/pages/custom_style_class.json.jbuilder +1 -1
- data/app/views/json_ui/garage/pages/nav_buttons.json.jbuilder +31 -13
- data/app/views/json_ui/garage/panels/_styled.json.jbuilder +8 -8
- data/app/views/json_ui/garage/panels/hover.json.jbuilder +2 -2
- data/app/views/json_ui/garage/panels/timeline.json.jbuilder +5 -5
- data/app/views/json_ui/garage/panels/ul.json.jbuilder +1 -1
- data/app/views/json_ui/garage/tables/bulk_edit.json.jbuilder +1 -1
- data/app/views/json_ui/garage/test_page/file_upload_new.json.jbuilder +1 -1
- data/app/views/json_ui/garage/test_page/form.json.jbuilder +6 -6
- data/app/views/json_ui/garage/test_page/form_dynamic.json.jbuilder +2 -2
- data/app/views/json_ui/garage/test_page/logics_set.json.jbuilder +94 -0
- data/app/views/json_ui/garage/views/components_replace.json.jbuilder +13 -13
- data/app/views/json_ui/garage/views/components_set.json.jbuilder +6 -6
- data/app/views/json_ui/garage/views/fields_focus.json.jbuilder +22 -22
- data/app/views/json_ui/garage/views/markdowns.json.jbuilder +2 -0
- data/config/routes.rb +4 -0
- data/lib/glib/doc_generator.rb +386 -0
- data/lib/glib/json_crawler/router.rb +45 -24
- data/lib/glib/rubocop/cops/json_ui/base_nested_parameter.rb +145 -0
- data/lib/glib/rubocop/cops/json_ui/nested_action_parameter.rb +55 -0
- data/lib/glib/rubocop/cops/json_ui/nested_block_parameter.rb +51 -0
- data/lib/glib/rubocop/cops/multiline_method_call_style.rb +74 -5
- data/lib/glib/rubocop/cops/test_name_parentheses.rb +33 -0
- data/lib/glib/rubocop.rb +4 -0
- data/lib/glib/test/parallel_coverage.rb +38 -0
- data/lib/glib/test_helpers.rb +12 -0
- data/lib/glib-web.rb +1 -0
- data/lib/tasks/db.rake +1 -1
- data/lib/tasks/docs.rake +59 -0
- 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,
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
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
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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
|
-
|
|
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
|