code-ruby 3.1.2 → 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 (98) 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 +15 -1
  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/object/base_64.rb +132 -6
  13. data/lib/code/object/boolean.rb +60 -0
  14. data/lib/code/object/class.rb +138 -2
  15. data/lib/code/object/code.rb +111 -3
  16. data/lib/code/object/context.rb +57 -1
  17. data/lib/code/object/cryptography.rb +63 -0
  18. data/lib/code/object/date.rb +13339 -462
  19. data/lib/code/object/decimal.rb +1725 -0
  20. data/lib/code/object/dictionary.rb +1790 -11
  21. data/lib/code/object/duration.rb +28 -0
  22. data/lib/code/object/function.rb +261 -23
  23. data/lib/code/object/global.rb +534 -1
  24. data/lib/code/object/html.rb +179 -7
  25. data/lib/code/object/http.rb +244 -14
  26. data/lib/code/object/ics.rb +75 -13
  27. data/lib/code/object/identifier_list.rb +17 -2
  28. data/lib/code/object/integer.rb +1937 -2
  29. data/lib/code/object/json.rb +75 -1
  30. data/lib/code/object/list.rb +3383 -10
  31. data/lib/code/object/nothing.rb +53 -0
  32. data/lib/code/object/number.rb +110 -0
  33. data/lib/code/object/parameter.rb +140 -0
  34. data/lib/code/object/range.rb +576 -14
  35. data/lib/code/object/smtp.rb +95 -12
  36. data/lib/code/object/string.rb +944 -3
  37. data/lib/code/object/super.rb +10 -1
  38. data/lib/code/object/time.rb +13358 -498
  39. data/lib/code/object/url.rb +65 -0
  40. data/lib/code/object.rb +543 -0
  41. data/lib/code/parser.rb +161 -24
  42. data/lib/code-ruby.rb +3 -0
  43. data/lib/code.rb +30 -3
  44. metadata +135 -84
  45. data/.github/dependabot.yml +0 -15
  46. data/.github/workflows/ci.yml +0 -38
  47. data/.gitignore +0 -30
  48. data/.node-version +0 -1
  49. data/.npm-version +0 -1
  50. data/.prettierignore +0 -2
  51. data/.rspec +0 -1
  52. data/.rubocop.yml +0 -140
  53. data/.ruby-version +0 -1
  54. data/.tool-versions +0 -3
  55. data/AGENTS.md +0 -43
  56. data/Gemfile +0 -22
  57. data/Gemfile.lock +0 -292
  58. data/Rakefile +0 -5
  59. data/bin/bundle +0 -123
  60. data/bin/bundle-audit +0 -31
  61. data/bin/bundler-audit +0 -31
  62. data/bin/dorian +0 -31
  63. data/bin/rspec +0 -31
  64. data/bin/rubocop +0 -31
  65. data/bin/test +0 -5
  66. data/code-ruby.gemspec +0 -34
  67. data/docs/precedence.txt +0 -36
  68. data/package-lock.json +0 -14
  69. data/package.json +0 -7
  70. data/spec/bin/code_spec.rb +0 -48
  71. data/spec/code/format_spec.rb +0 -153
  72. data/spec/code/node/call_spec.rb +0 -11
  73. data/spec/code/object/boolean_spec.rb +0 -18
  74. data/spec/code/object/cryptography_spec.rb +0 -25
  75. data/spec/code/object/decimal_spec.rb +0 -50
  76. data/spec/code/object/dictionary_spec.rb +0 -98
  77. data/spec/code/object/function_spec.rb +0 -268
  78. data/spec/code/object/http_spec.rb +0 -33
  79. data/spec/code/object/ics_spec.rb +0 -50
  80. data/spec/code/object/integer_spec.rb +0 -42
  81. data/spec/code/object/list_spec.rb +0 -22
  82. data/spec/code/object/nothing_spec.rb +0 -14
  83. data/spec/code/object/range_spec.rb +0 -23
  84. data/spec/code/object/string_spec.rb +0 -26
  85. data/spec/code/parser/boolean_spec.rb +0 -11
  86. data/spec/code/parser/chained_call_spec.rb +0 -16
  87. data/spec/code/parser/dictionary_spec.rb +0 -18
  88. data/spec/code/parser/function_spec.rb +0 -16
  89. data/spec/code/parser/group_spec.rb +0 -11
  90. data/spec/code/parser/if_modifier_spec.rb +0 -18
  91. data/spec/code/parser/list_spec.rb +0 -17
  92. data/spec/code/parser/number_spec.rb +0 -11
  93. data/spec/code/parser/string_spec.rb +0 -20
  94. data/spec/code/parser_spec.rb +0 -52
  95. data/spec/code/type_spec.rb +0 -21
  96. data/spec/code_spec.rb +0 -717
  97. data/spec/spec_helper.rb +0 -21
  98. 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
@@ -565,6 +575,19 @@ class Code
565
575
  end
566
576
 
567
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
+
568
591
  if label_name_start?(current) && next_token_value == ":"
569
592
  name = advance.value
570
593
  advance
@@ -653,10 +676,11 @@ class Code
653
676
 
654
677
  {
655
678
  name: name,
656
- regular_splat: (true if prefix == "*"),
657
- keyword_splat: (true if prefix == "**"),
658
- block: (true if prefix == "&"),
659
- 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)),
660
684
  default: default
661
685
  }.compact
662
686
  end
@@ -664,7 +688,7 @@ class Code
664
688
  def parse_parameter_prefix
665
689
  return unless current.type == :operator
666
690
 
667
- advance.value if %w[* ** & .. ... .].include?(current.value)
691
+ advance.value if %w[* ** & && .. ... .].include?(current.value)
668
692
  end
669
693
 
670
694
  def parse_optional_code(stop_values)
@@ -952,6 +976,65 @@ class Code
952
976
  raise Error, "#{message} at #{token.position}"
953
977
  end
954
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
+
955
1038
  def lex(source)
956
1039
  tokens = []
957
1040
  index = 0
@@ -1200,10 +1283,16 @@ class Code
1200
1283
  end
1201
1284
 
1202
1285
  if char == "{"
1203
- parts << { type: :text, value: text } unless text.empty?
1204
- text = +""
1205
- code, i = extract_braced(source, i)
1206
- 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
1207
1296
  next
1208
1297
  end
1209
1298
 
@@ -1219,28 +1308,74 @@ class Code
1219
1308
  depth = 1
1220
1309
  i = index + 1
1221
1310
  body = +""
1311
+ quote = nil
1312
+ escaped = false
1222
1313
 
1223
1314
  while i < source.length
1224
1315
  char = source[i]
1225
1316
 
1226
- 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 == "{"
1227
1365
  depth += 1
1228
1366
  elsif char == "}"
1229
1367
  depth -= 1
1230
- return body, i + 1 if depth.zero?
1368
+ return body, i + 1, true if depth.zero?
1231
1369
  end
1232
1370
  body << char
1233
1371
  i += 1
1234
1372
  end
1235
1373
 
1236
- [body, i]
1374
+ [body, i, false]
1237
1375
  end
1238
1376
 
1239
1377
  def scan_number(source, index)
1240
- rest = source[index..]
1241
- return unless rest
1242
-
1243
- 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))
1244
1379
  return(
1245
1380
  {
1246
1381
  raw: {
@@ -1253,7 +1388,7 @@ class Code
1253
1388
  )
1254
1389
  end
1255
1390
 
1256
- if (match = /\A0[oO][0-7](?:_?[0-7])*/.match(rest))
1391
+ if (match = /\G0[oO][0-7](?:_?[0-7])*/.match(source, index))
1257
1392
  return(
1258
1393
  {
1259
1394
  raw: {
@@ -1266,7 +1401,7 @@ class Code
1266
1401
  )
1267
1402
  end
1268
1403
 
1269
- if (match = /\A0[bB][01](?:_?[01])*/.match(rest))
1404
+ if (match = /\G0[bB][01](?:_?[01])*/.match(source, index))
1270
1405
  return(
1271
1406
  {
1272
1407
  raw: {
@@ -1281,8 +1416,9 @@ class Code
1281
1416
 
1282
1417
  if (
1283
1418
  match =
1284
- /\A[0-9](?:_?[0-9])*\.[0-9](?:_?[0-9])*(?:[eE][0-9](?:_?[0-9])*(?:\.[0-9](?:_?[0-9])*)?)?/.match(
1285
- 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
1286
1422
  )
1287
1423
  )
1288
1424
  decimal, exponent = match[0].split(/[eE]/, 2)
@@ -1295,8 +1431,9 @@ class Code
1295
1431
 
1296
1432
  if (
1297
1433
  match =
1298
- /\A[0-9](?:_?[0-9])*(?:[eE][0-9](?:_?[0-9])*(?:\.[0-9](?:_?[0-9])*)?)?/.match(
1299
- rest
1434
+ /\G[0-9](?:_?[0-9])*(?:[eE][0-9](?:_?[0-9])*(?:\.[0-9](?:_?[0-9])*)?)?/.match(
1435
+ source,
1436
+ index
1300
1437
  )
1301
1438
  )
1302
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