code-ruby 3.1.2 → 4.0.1

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 +100 -20
  4. data/lib/code/concerns/shared.rb +335 -15
  5. data/lib/code/format.rb +33 -15
  6. data/lib/code/network.rb +82 -0
  7. data/lib/code/node/call.rb +80 -2
  8. data/lib/code/node/call_argument.rb +14 -0
  9. data/lib/code/node/code.rb +4 -3
  10. data/lib/code/node/function_parameter.rb +7 -4
  11. data/lib/code/node/list.rb +32 -2
  12. data/lib/code/node/square_bracket.rb +4 -2
  13. data/lib/code/object/base_64.rb +132 -6
  14. data/lib/code/object/boolean.rb +56 -0
  15. data/lib/code/object/class.rb +143 -2
  16. data/lib/code/object/code.rb +108 -7
  17. data/lib/code/object/context.rb +59 -1
  18. data/lib/code/object/cryptography.rb +69 -0
  19. data/lib/code/object/date.rb +13800 -462
  20. data/lib/code/object/decimal.rb +1098 -0
  21. data/lib/code/object/dictionary.rb +1861 -11
  22. data/lib/code/object/duration.rb +24 -0
  23. data/lib/code/object/function.rb +289 -27
  24. data/lib/code/object/global.rb +447 -1
  25. data/lib/code/object/html.rb +181 -7
  26. data/lib/code/object/http.rb +253 -17
  27. data/lib/code/object/ics.rb +76 -13
  28. data/lib/code/object/identifier_list.rb +30 -10
  29. data/lib/code/object/integer.rb +1265 -2
  30. data/lib/code/object/json.rb +80 -1
  31. data/lib/code/object/list.rb +3371 -10
  32. data/lib/code/object/nothing.rb +53 -0
  33. data/lib/code/object/number.rb +120 -0
  34. data/lib/code/object/parameter.rb +149 -0
  35. data/lib/code/object/range.rb +530 -14
  36. data/lib/code/object/smtp.rb +103 -12
  37. data/lib/code/object/string.rb +968 -3
  38. data/lib/code/object/super.rb +11 -1
  39. data/lib/code/object/time.rb +13932 -498
  40. data/lib/code/object/url.rb +67 -0
  41. data/lib/code/object.rb +582 -0
  42. data/lib/code/parser.rb +194 -55
  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 -717
  98. data/spec/spec_helper.rb +0 -21
  99. data/spec/zeitwerk/loader_spec.rb +0 -7
data/lib/code/parser.rb CHANGED
@@ -32,37 +32,37 @@ class Code
32
32
  while
33
33
  ].freeze
34
34
 
35
- MULTI_CHAR_OPERATORS = [
36
- "&.",
37
- "&&",
38
- "&&=",
39
- "**",
40
- "*=",
41
- "+=",
42
- "-=",
43
- "..",
44
- "...",
45
- "/=",
46
- "::",
47
- "<<=",
48
- "<<",
49
- "<=>",
50
- "<=",
51
- "===",
52
- "==",
53
- "=~",
54
- ">=",
55
- ">>=",
56
- ">>",
57
- "||=",
58
- "||",
59
- "|=",
60
- "!==",
61
- "!=",
62
- "!~",
63
- "%=",
64
- "^=",
65
- "=>"
35
+ MULTI_CHAR_OPERATORS = %w[
36
+ &.
37
+ &&
38
+ &&=
39
+ **
40
+ *=
41
+ +=
42
+ -=
43
+ ..
44
+ ...
45
+ /=
46
+ ::
47
+ <<=
48
+ <<
49
+ <=>
50
+ <=
51
+ ===
52
+ ==
53
+ =~
54
+ >=
55
+ >>=
56
+ >>
57
+ ||=
58
+ ||
59
+ |=
60
+ !==
61
+ !=
62
+ !~
63
+ %=
64
+ ^=
65
+ =>
66
66
  ].sort_by(&:length).reverse.freeze
67
67
  CONTINUATION_KEYWORDS = %w[or and rescue].freeze
68
68
  POSTFIX_CONTINUATIONS = %w[. :: &.].freeze
@@ -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,67 @@ 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
+ if depth > MAX_NESTING
1019
+ raise_parse_error_at("source is too deeply nested", index)
1020
+ end
1021
+ elsif ")]}".include?(char)
1022
+ depth -= 1 if depth.positive?
1023
+ end
1024
+ index += 1
1025
+ end
1026
+ end
1027
+
1028
+ def raise_parse_error_at(message, position)
1029
+ token =
1030
+ Token.new(
1031
+ type: :unknown,
1032
+ value: "",
1033
+ position: position,
1034
+ newline_before: false,
1035
+ space_before: false
1036
+ )
1037
+ raise_parse_error(message, token)
1038
+ end
1039
+
955
1040
  def lex(source)
956
1041
  tokens = []
957
1042
  index = 0
@@ -1200,10 +1285,16 @@ class Code
1200
1285
  end
1201
1286
 
1202
1287
  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 }
1288
+ code, i, closed = extract_braced(source, i)
1289
+
1290
+ if closed
1291
+ parts << { type: :text, value: text } unless text.empty?
1292
+ text = +""
1293
+ parts << { type: :code, value: code }
1294
+ else
1295
+ text << "{" << code
1296
+ text << "}" if closed
1297
+ end
1207
1298
  next
1208
1299
  end
1209
1300
 
@@ -1219,28 +1310,74 @@ class Code
1219
1310
  depth = 1
1220
1311
  i = index + 1
1221
1312
  body = +""
1313
+ quote = nil
1314
+ escaped = false
1222
1315
 
1223
1316
  while i < source.length
1224
1317
  char = source[i]
1225
1318
 
1226
- if char == "{"
1319
+ if quote
1320
+ body << char
1321
+ if escaped
1322
+ escaped = false
1323
+ elsif char == "\\"
1324
+ escaped = true
1325
+ elsif char == quote
1326
+ quote = nil
1327
+ end
1328
+ i += 1
1329
+ next
1330
+ end
1331
+
1332
+ if %w[' "].include?(char)
1333
+ quote = char
1334
+ elsif char == "#"
1335
+ while i < source.length && !NEWLINE_CHARACTERS.include?(source[i])
1336
+ body << source[i]
1337
+ i += 1
1338
+ end
1339
+ next
1340
+ elsif source[i, 2] == "//"
1341
+ 2.times do
1342
+ body << source[i]
1343
+ i += 1
1344
+ end
1345
+ while i < source.length && !NEWLINE_CHARACTERS.include?(source[i])
1346
+ body << source[i]
1347
+ i += 1
1348
+ end
1349
+ next
1350
+ elsif source[i, 2] == "/*"
1351
+ 2.times do
1352
+ body << source[i]
1353
+ i += 1
1354
+ end
1355
+ while i < source.length && source[i, 2] != "*/"
1356
+ body << source[i]
1357
+ i += 1
1358
+ end
1359
+ if source[i, 2] == "*/"
1360
+ 2.times do
1361
+ body << source[i]
1362
+ i += 1
1363
+ end
1364
+ end
1365
+ next
1366
+ elsif char == "{"
1227
1367
  depth += 1
1228
1368
  elsif char == "}"
1229
1369
  depth -= 1
1230
- return body, i + 1 if depth.zero?
1370
+ return body, i + 1, true if depth.zero?
1231
1371
  end
1232
1372
  body << char
1233
1373
  i += 1
1234
1374
  end
1235
1375
 
1236
- [body, i]
1376
+ [body, i, false]
1237
1377
  end
1238
1378
 
1239
1379
  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))
1380
+ if (match = /\G0[xX][0-9a-fA-F](?:_?[0-9a-fA-F])*/.match(source, index))
1244
1381
  return(
1245
1382
  {
1246
1383
  raw: {
@@ -1253,7 +1390,7 @@ class Code
1253
1390
  )
1254
1391
  end
1255
1392
 
1256
- if (match = /\A0[oO][0-7](?:_?[0-7])*/.match(rest))
1393
+ if (match = /\G0[oO][0-7](?:_?[0-7])*/.match(source, index))
1257
1394
  return(
1258
1395
  {
1259
1396
  raw: {
@@ -1266,7 +1403,7 @@ class Code
1266
1403
  )
1267
1404
  end
1268
1405
 
1269
- if (match = /\A0[bB][01](?:_?[01])*/.match(rest))
1406
+ if (match = /\G0[bB][01](?:_?[01])*/.match(source, index))
1270
1407
  return(
1271
1408
  {
1272
1409
  raw: {
@@ -1281,8 +1418,9 @@ class Code
1281
1418
 
1282
1419
  if (
1283
1420
  match =
1284
- /\A[0-9](?:_?[0-9])*\.[0-9](?:_?[0-9])*(?:[eE][0-9](?:_?[0-9])*(?:\.[0-9](?:_?[0-9])*)?)?/.match(
1285
- rest
1421
+ /\G[0-9](?:_?[0-9])*\.[0-9](?:_?[0-9])*(?:[eE][0-9](?:_?[0-9])*(?:\.[0-9](?:_?[0-9])*)?)?/.match(
1422
+ source,
1423
+ index
1286
1424
  )
1287
1425
  )
1288
1426
  decimal, exponent = match[0].split(/[eE]/, 2)
@@ -1295,8 +1433,9 @@ class Code
1295
1433
 
1296
1434
  if (
1297
1435
  match =
1298
- /\A[0-9](?:_?[0-9])*(?:[eE][0-9](?:_?[0-9])*(?:\.[0-9](?:_?[0-9])*)?)?/.match(
1299
- rest
1436
+ /\G[0-9](?:_?[0-9])*(?:[eE][0-9](?:_?[0-9])*(?:\.[0-9](?:_?[0-9])*)?)?/.match(
1437
+ source,
1438
+ index
1300
1439
  )
1301
1440
  )
1302
1441
  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