code-ruby 3.1.1 → 4.0.0

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 (99) hide show
  1. checksums.yaml +4 -4
  2. data/VERSION +1 -1
  3. data/bin/code +97 -20
  4. data/lib/code/concerns/shared.rb +331 -15
  5. data/lib/code/format.rb +22 -2
  6. data/lib/code/network.rb +87 -0
  7. data/lib/code/node/call.rb +79 -2
  8. data/lib/code/node/call_argument.rb +14 -0
  9. data/lib/code/node/code.rb +5 -4
  10. data/lib/code/node/function_parameter.rb +7 -4
  11. data/lib/code/node/list.rb +29 -1
  12. data/lib/code/node/while.rb +21 -0
  13. data/lib/code/object/base_64.rb +132 -6
  14. data/lib/code/object/boolean.rb +60 -0
  15. data/lib/code/object/class.rb +138 -2
  16. data/lib/code/object/code.rb +111 -3
  17. data/lib/code/object/context.rb +57 -1
  18. data/lib/code/object/cryptography.rb +63 -0
  19. data/lib/code/object/date.rb +13339 -462
  20. data/lib/code/object/decimal.rb +1725 -0
  21. data/lib/code/object/dictionary.rb +1835 -12
  22. data/lib/code/object/duration.rb +28 -0
  23. data/lib/code/object/function.rb +261 -23
  24. data/lib/code/object/global.rb +534 -1
  25. data/lib/code/object/html.rb +227 -16
  26. data/lib/code/object/http.rb +244 -14
  27. data/lib/code/object/ics.rb +75 -13
  28. data/lib/code/object/identifier_list.rb +17 -2
  29. data/lib/code/object/integer.rb +1941 -2
  30. data/lib/code/object/json.rb +75 -1
  31. data/lib/code/object/list.rb +3417 -10
  32. data/lib/code/object/nothing.rb +53 -0
  33. data/lib/code/object/number.rb +110 -0
  34. data/lib/code/object/parameter.rb +140 -0
  35. data/lib/code/object/range.rb +596 -14
  36. data/lib/code/object/smtp.rb +95 -12
  37. data/lib/code/object/string.rb +944 -3
  38. data/lib/code/object/super.rb +10 -1
  39. data/lib/code/object/time.rb +13358 -498
  40. data/lib/code/object/url.rb +65 -0
  41. data/lib/code/object.rb +543 -0
  42. data/lib/code/parser.rb +177 -26
  43. data/lib/code-ruby.rb +3 -0
  44. data/lib/code.rb +30 -3
  45. metadata +135 -84
  46. data/.github/dependabot.yml +0 -15
  47. data/.github/workflows/ci.yml +0 -38
  48. data/.gitignore +0 -30
  49. data/.node-version +0 -1
  50. data/.npm-version +0 -1
  51. data/.prettierignore +0 -2
  52. data/.rspec +0 -1
  53. data/.rubocop.yml +0 -140
  54. data/.ruby-version +0 -1
  55. data/.tool-versions +0 -3
  56. data/AGENTS.md +0 -43
  57. data/Gemfile +0 -22
  58. data/Gemfile.lock +0 -292
  59. data/Rakefile +0 -5
  60. data/bin/bundle +0 -123
  61. data/bin/bundle-audit +0 -31
  62. data/bin/bundler-audit +0 -31
  63. data/bin/dorian +0 -31
  64. data/bin/rspec +0 -31
  65. data/bin/rubocop +0 -31
  66. data/bin/test +0 -5
  67. data/code-ruby.gemspec +0 -34
  68. data/docs/precedence.txt +0 -36
  69. data/package-lock.json +0 -14
  70. data/package.json +0 -7
  71. data/spec/bin/code_spec.rb +0 -48
  72. data/spec/code/format_spec.rb +0 -153
  73. data/spec/code/node/call_spec.rb +0 -11
  74. data/spec/code/object/boolean_spec.rb +0 -18
  75. data/spec/code/object/cryptography_spec.rb +0 -25
  76. data/spec/code/object/decimal_spec.rb +0 -50
  77. data/spec/code/object/dictionary_spec.rb +0 -98
  78. data/spec/code/object/function_spec.rb +0 -268
  79. data/spec/code/object/http_spec.rb +0 -33
  80. data/spec/code/object/ics_spec.rb +0 -50
  81. data/spec/code/object/integer_spec.rb +0 -42
  82. data/spec/code/object/list_spec.rb +0 -22
  83. data/spec/code/object/nothing_spec.rb +0 -14
  84. data/spec/code/object/range_spec.rb +0 -23
  85. data/spec/code/object/string_spec.rb +0 -26
  86. data/spec/code/parser/boolean_spec.rb +0 -11
  87. data/spec/code/parser/chained_call_spec.rb +0 -16
  88. data/spec/code/parser/dictionary_spec.rb +0 -18
  89. data/spec/code/parser/function_spec.rb +0 -16
  90. data/spec/code/parser/group_spec.rb +0 -11
  91. data/spec/code/parser/if_modifier_spec.rb +0 -18
  92. data/spec/code/parser/list_spec.rb +0 -17
  93. data/spec/code/parser/number_spec.rb +0 -11
  94. data/spec/code/parser/string_spec.rb +0 -20
  95. data/spec/code/parser_spec.rb +0 -52
  96. data/spec/code/type_spec.rb +0 -21
  97. data/spec/code_spec.rb +0 -642
  98. data/spec/spec_helper.rb +0 -21
  99. data/spec/zeitwerk/loader_spec.rb +0 -7
data/lib/code/parser.rb CHANGED
@@ -73,7 +73,7 @@ class Code
73
73
  SUFFIX_PUNCTUATION = %w[! ?].freeze
74
74
 
75
75
  ASSIGNMENT_RHS_MIN_BP = 20
76
-
76
+ MAX_NESTING = 200
77
77
  INFIX_PRECEDENCE = {
78
78
  "if" => [10, 9],
79
79
  "unless" => [10, 9],
@@ -129,6 +129,7 @@ class Code
129
129
 
130
130
  def initialize(input)
131
131
  @input = input.to_s
132
+ ensure_source_nesting_limit!(@input)
132
133
  @tokens = lex(@input)
133
134
  @index = 0
134
135
  end
@@ -141,6 +142,10 @@ class Code
141
142
  Node::Code.new(parse_code)
142
143
  end
143
144
 
145
+ def lex_source(source)
146
+ lex(source.to_s)
147
+ end
148
+
144
149
  private
145
150
 
146
151
  attr_reader :input, :tokens
@@ -284,7 +289,12 @@ class Code
284
289
  case current.value
285
290
  when ".", "::", "&."
286
291
  operator = advance.value
287
- statement = parse_expression(151)
292
+ statement =
293
+ if current.type == :keyword
294
+ { call: { name: advance.value } }
295
+ else
296
+ parse_expression(151)
297
+ end
288
298
  append_left_operation(left, operator, statement)
289
299
  when "["
290
300
  advance
@@ -407,12 +417,26 @@ class Code
407
417
  skip_newlines
408
418
 
409
419
  statement = parse_expression unless operator == "loop"
410
- body = parse_body(%w[end])
420
+
421
+ if operator == "loop" &&
422
+ (match?(:punctuation, "{") || match?(:keyword, "do"))
423
+ block = parse_block(current.value)
424
+ body = block[:body]
425
+ parameters = block[:parameters]
426
+ else
427
+ body = parse_body(%w[end])
428
+ end
429
+
411
430
  skip_newlines
412
431
  advance if match?(:keyword, "end")
413
432
 
414
433
  {
415
- while: { operator: operator, statement: statement, body: body }.compact
434
+ while: {
435
+ operator: operator,
436
+ statement: statement,
437
+ parameters: parameters,
438
+ body: body
439
+ }.compact
416
440
  }
417
441
  end
418
442
 
@@ -551,6 +575,19 @@ class Code
551
575
  end
552
576
 
553
577
  def parse_argument
578
+ if current.type == :operator && %w[* ** & && ...].include?(current.value)
579
+ operator = advance.value
580
+ value =
581
+ if %w[, )].include?(next_significant_token.value)
582
+ skip_newlines
583
+ nil
584
+ else
585
+ parse_code(stop_values: %w[, )])
586
+ end
587
+
588
+ return { operator: operator, value: value }.compact
589
+ end
590
+
554
591
  if label_name_start?(current) && next_token_value == ":"
555
592
  name = advance.value
556
593
  advance
@@ -639,10 +676,11 @@ class Code
639
676
 
640
677
  {
641
678
  name: name,
642
- regular_splat: (true if prefix == "*"),
643
- keyword_splat: (true if prefix == "**"),
644
- block: (true if prefix == "&"),
645
- spread: (true if %w[. .. ...].include?(prefix)),
679
+ regular_splat: (prefix if prefix == "*"),
680
+ keyword_splat: (prefix if prefix == "**"),
681
+ block: (prefix if prefix == "&"),
682
+ blocks: (prefix if prefix == "&&"),
683
+ spread: (prefix if %w[. .. ...].include?(prefix)),
646
684
  default: default
647
685
  }.compact
648
686
  end
@@ -650,7 +688,7 @@ class Code
650
688
  def parse_parameter_prefix
651
689
  return unless current.type == :operator
652
690
 
653
- advance.value if %w[* ** & .. ... .].include?(current.value)
691
+ advance.value if %w[* ** & && .. ... .].include?(current.value)
654
692
  end
655
693
 
656
694
  def parse_optional_code(stop_values)
@@ -938,6 +976,65 @@ class Code
938
976
  raise Error, "#{message} at #{token.position}"
939
977
  end
940
978
 
979
+ def ensure_source_nesting_limit!(source)
980
+ depth = 0
981
+ quote = nil
982
+ escaped = false
983
+ index = 0
984
+
985
+ while index < source.length
986
+ char = source[index]
987
+ if quote
988
+ if escaped
989
+ escaped = false
990
+ elsif char == "\\"
991
+ escaped = true
992
+ elsif char == quote
993
+ quote = nil
994
+ end
995
+ index += 1
996
+ next
997
+ end
998
+
999
+ if %w[' "].include?(char)
1000
+ quote = char
1001
+ elsif char == "#"
1002
+ index += 1
1003
+ index += 1 while index < source.length &&
1004
+ !NEWLINE_CHARACTERS.include?(source[index])
1005
+ next
1006
+ elsif source[index, 2] == "//"
1007
+ index += 2
1008
+ index += 1 while index < source.length &&
1009
+ !NEWLINE_CHARACTERS.include?(source[index])
1010
+ next
1011
+ elsif source[index, 2] == "/*"
1012
+ index += 2
1013
+ index += 1 while index < source.length && source[index, 2] != "*/"
1014
+ index += 2 if source[index, 2] == "*/"
1015
+ next
1016
+ elsif "([{".include?(char)
1017
+ depth += 1
1018
+ raise_parse_error_at("source is too deeply nested", index) if depth > MAX_NESTING
1019
+ elsif ")]}".include?(char)
1020
+ depth -= 1 if depth.positive?
1021
+ end
1022
+ index += 1
1023
+ end
1024
+ end
1025
+
1026
+ def raise_parse_error_at(message, position)
1027
+ token =
1028
+ Token.new(
1029
+ type: :unknown,
1030
+ value: "",
1031
+ position: position,
1032
+ newline_before: false,
1033
+ space_before: false
1034
+ )
1035
+ raise_parse_error(message, token)
1036
+ end
1037
+
941
1038
  def lex(source)
942
1039
  tokens = []
943
1040
  index = 0
@@ -1186,10 +1283,16 @@ class Code
1186
1283
  end
1187
1284
 
1188
1285
  if char == "{"
1189
- parts << { type: :text, value: text } unless text.empty?
1190
- text = +""
1191
- code, i = extract_braced(source, i)
1192
- parts << { type: :code, value: code }
1286
+ code, i, closed = extract_braced(source, i)
1287
+
1288
+ if closed
1289
+ parts << { type: :text, value: text } unless text.empty?
1290
+ text = +""
1291
+ parts << { type: :code, value: code }
1292
+ else
1293
+ text << "{" << code
1294
+ text << "}" if closed
1295
+ end
1193
1296
  next
1194
1297
  end
1195
1298
 
@@ -1205,28 +1308,74 @@ class Code
1205
1308
  depth = 1
1206
1309
  i = index + 1
1207
1310
  body = +""
1311
+ quote = nil
1312
+ escaped = false
1208
1313
 
1209
1314
  while i < source.length
1210
1315
  char = source[i]
1211
1316
 
1212
- if char == "{"
1317
+ if quote
1318
+ body << char
1319
+ if escaped
1320
+ escaped = false
1321
+ elsif char == "\\"
1322
+ escaped = true
1323
+ elsif char == quote
1324
+ quote = nil
1325
+ end
1326
+ i += 1
1327
+ next
1328
+ end
1329
+
1330
+ if %w[' "].include?(char)
1331
+ quote = char
1332
+ elsif char == "#"
1333
+ while i < source.length && !NEWLINE_CHARACTERS.include?(source[i])
1334
+ body << source[i]
1335
+ i += 1
1336
+ end
1337
+ next
1338
+ elsif source[i, 2] == "//"
1339
+ 2.times do
1340
+ body << source[i]
1341
+ i += 1
1342
+ end
1343
+ while i < source.length && !NEWLINE_CHARACTERS.include?(source[i])
1344
+ body << source[i]
1345
+ i += 1
1346
+ end
1347
+ next
1348
+ elsif source[i, 2] == "/*"
1349
+ 2.times do
1350
+ body << source[i]
1351
+ i += 1
1352
+ end
1353
+ while i < source.length && source[i, 2] != "*/"
1354
+ body << source[i]
1355
+ i += 1
1356
+ end
1357
+ if source[i, 2] == "*/"
1358
+ 2.times do
1359
+ body << source[i]
1360
+ i += 1
1361
+ end
1362
+ end
1363
+ next
1364
+ elsif char == "{"
1213
1365
  depth += 1
1214
1366
  elsif char == "}"
1215
1367
  depth -= 1
1216
- return body, i + 1 if depth.zero?
1368
+ return body, i + 1, true if depth.zero?
1217
1369
  end
1218
1370
  body << char
1219
1371
  i += 1
1220
1372
  end
1221
1373
 
1222
- [body, i]
1374
+ [body, i, false]
1223
1375
  end
1224
1376
 
1225
1377
  def scan_number(source, index)
1226
- rest = source[index..]
1227
- return unless rest
1228
-
1229
- if (match = /\A0[xX][0-9a-fA-F](?:_?[0-9a-fA-F])*/.match(rest))
1378
+ if (match = /\G0[xX][0-9a-fA-F](?:_?[0-9a-fA-F])*/.match(source, index))
1230
1379
  return(
1231
1380
  {
1232
1381
  raw: {
@@ -1239,7 +1388,7 @@ class Code
1239
1388
  )
1240
1389
  end
1241
1390
 
1242
- if (match = /\A0[oO][0-7](?:_?[0-7])*/.match(rest))
1391
+ if (match = /\G0[oO][0-7](?:_?[0-7])*/.match(source, index))
1243
1392
  return(
1244
1393
  {
1245
1394
  raw: {
@@ -1252,7 +1401,7 @@ class Code
1252
1401
  )
1253
1402
  end
1254
1403
 
1255
- if (match = /\A0[bB][01](?:_?[01])*/.match(rest))
1404
+ if (match = /\G0[bB][01](?:_?[01])*/.match(source, index))
1256
1405
  return(
1257
1406
  {
1258
1407
  raw: {
@@ -1267,8 +1416,9 @@ class Code
1267
1416
 
1268
1417
  if (
1269
1418
  match =
1270
- /\A[0-9](?:_?[0-9])*\.[0-9](?:_?[0-9])*(?:[eE][0-9](?:_?[0-9])*(?:\.[0-9](?:_?[0-9])*)?)?/.match(
1271
- rest
1419
+ /\G[0-9](?:_?[0-9])*\.[0-9](?:_?[0-9])*(?:[eE][0-9](?:_?[0-9])*(?:\.[0-9](?:_?[0-9])*)?)?/.match(
1420
+ source,
1421
+ index
1272
1422
  )
1273
1423
  )
1274
1424
  decimal, exponent = match[0].split(/[eE]/, 2)
@@ -1281,8 +1431,9 @@ class Code
1281
1431
 
1282
1432
  if (
1283
1433
  match =
1284
- /\A[0-9](?:_?[0-9])*(?:[eE][0-9](?:_?[0-9])*(?:\.[0-9](?:_?[0-9])*)?)?/.match(
1285
- rest
1434
+ /\G[0-9](?:_?[0-9])*(?:[eE][0-9](?:_?[0-9])*(?:\.[0-9](?:_?[0-9])*)?)?/.match(
1435
+ source,
1436
+ index
1286
1437
  )
1287
1438
  )
1288
1439
  whole, exponent = match[0].split(/[eE]/, 2)
data/lib/code-ruby.rb CHANGED
@@ -8,10 +8,13 @@ require "date"
8
8
  require "did_you_mean"
9
9
  require "digest"
10
10
  require "icalendar"
11
+ require "ipaddr"
11
12
  require "json"
12
13
  require "mail"
13
14
  require "net/http"
14
15
  require "nokogiri"
16
+ require "openssl"
17
+ require "resolv"
15
18
  require "stringio"
16
19
  require "timeout"
17
20
  require "uri"
data/lib/code.rb CHANGED
@@ -1,8 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class Code
4
- GLOBALS = %i[context error input object output source].freeze
5
- DEFAULT_TIMEOUT = 0
4
+ GLOBALS = %i[context error input object output root_object source].freeze
5
+ DEFAULT_TIMEOUT = 1.hour.to_f
6
+ MAX_INPUT_BYTES = 10.megabytes
6
7
  LOCALES = %w[en fr].freeze
7
8
 
8
9
  def initialize(
@@ -20,13 +21,18 @@ class Code
20
21
  @object = object
21
22
  @output = output
22
23
  @source = source
23
- @timeout = timeout
24
+ @timeout = self.class.normalize_timeout!(timeout)
24
25
  end
25
26
 
26
27
  def self.parse(source, timeout: DEFAULT_TIMEOUT)
28
+ timeout = normalize_timeout!(timeout)
29
+
30
+ ensure_input_size!(source, label: "source")
27
31
  Timeout.timeout(timeout) { Parser.parse(source).to_raw }
28
32
  rescue Timeout::Error
29
33
  raise Error, "timeout"
34
+ rescue SystemStackError
35
+ raise Error, "source is too deeply nested"
30
36
  end
31
37
 
32
38
  def self.evaluate(...)
@@ -44,7 +50,23 @@ class Code
44
50
  Format.format(parse_tree)
45
51
  end
46
52
 
53
+ def self.ensure_input_size!(source, limit: MAX_INPUT_BYTES, label: "input")
54
+ return if source.to_s.bytesize <= limit
55
+
56
+ raise Error, "#{label} is too large"
57
+ end
58
+
59
+ def self.normalize_timeout!(timeout)
60
+ timeout = DEFAULT_TIMEOUT if timeout.nil?
61
+ timeout = timeout.to_f
62
+ raise Error, "timeout must be positive" unless timeout.positive?
63
+
64
+ timeout
65
+ end
66
+
47
67
  def evaluate
68
+ time_zone = ::Time.zone
69
+
48
70
  Timeout.timeout(timeout) do
49
71
  Node::Code.new(Code.parse(source)).evaluate(
50
72
  context: context,
@@ -53,6 +75,7 @@ class Code
53
75
  input: input,
54
76
  object: object,
55
77
  output: output,
78
+ root_object: object,
56
79
  source: source,
57
80
  timeout: timeout
58
81
  )
@@ -61,6 +84,10 @@ class Code
61
84
  raise Error, "timeout"
62
85
  rescue Interrupt
63
86
  raise Error, "interrupt"
87
+ rescue SystemStackError
88
+ raise Error, "source is too deeply nested"
89
+ ensure
90
+ ::Time.zone = time_zone
64
91
  end
65
92
 
66
93
  private