gloss 0.0.5 → 0.1.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (53) hide show
  1. checksums.yaml +4 -4
  2. data/.gitattributes +3 -0
  3. data/.github/workflows/{crystal.yml → crystal_specs.yml} +1 -1
  4. data/.github/workflows/{ruby.yml → ruby_specs.yml} +2 -2
  5. data/.github/workflows/self_build.yml +45 -0
  6. data/.gloss.yml +1 -0
  7. data/Gemfile.lock +3 -3
  8. data/README.md +35 -5
  9. data/Rakefile +1 -1
  10. data/exe/gloss +13 -2
  11. data/ext/gloss/Makefile +8 -19
  12. data/ext/gloss/lib/cr_ruby.cr +5 -4
  13. data/ext/gloss/src/cr_ast.cr +61 -77
  14. data/ext/gloss/src/gloss.cr +7 -3
  15. data/ext/gloss/src/rb_ast.cr +37 -36
  16. data/lib/gloss.rb +11 -7
  17. data/lib/gloss/cli.rb +61 -23
  18. data/lib/gloss/config.rb +3 -1
  19. data/lib/gloss/errors.rb +1 -1
  20. data/lib/gloss/initializer.rb +2 -1
  21. data/lib/gloss/logger.rb +29 -0
  22. data/lib/gloss/parser.rb +17 -2
  23. data/lib/gloss/prog_loader.rb +141 -0
  24. data/lib/gloss/scope.rb +1 -1
  25. data/lib/gloss/source.rb +1 -1
  26. data/lib/gloss/type_checker.rb +80 -32
  27. data/lib/gloss/utils.rb +44 -0
  28. data/lib/gloss/version.rb +4 -4
  29. data/lib/gloss/{builder.rb → visitor.rb} +93 -54
  30. data/lib/gloss/watcher.rb +41 -19
  31. data/lib/gloss/writer.rb +21 -10
  32. data/sig/core.rbs +2 -0
  33. data/sig/fast_blank.rbs +4 -0
  34. data/sig/{gloss.rbs → gls.rbs} +0 -0
  35. data/sig/optparse.rbs +6 -0
  36. data/sig/rubygems.rbs +9 -0
  37. data/sig/yaml.rbs +3 -0
  38. data/src/exe/gloss +19 -0
  39. data/src/lib/gloss.gl +25 -0
  40. data/src/lib/gloss/cli.gl +40 -14
  41. data/src/lib/gloss/config.gl +2 -2
  42. data/src/lib/gloss/initializer.gl +1 -1
  43. data/src/lib/gloss/logger.gl +21 -0
  44. data/src/lib/gloss/parser.gl +17 -5
  45. data/src/lib/gloss/prog_loader.gl +133 -0
  46. data/src/lib/gloss/scope.gl +0 -2
  47. data/src/lib/gloss/type_checker.gl +85 -39
  48. data/src/lib/gloss/utils.gl +38 -0
  49. data/src/lib/gloss/version.gl +1 -1
  50. data/src/lib/gloss/{builder.gl → visitor.gl} +80 -49
  51. data/src/lib/gloss/watcher.gl +42 -24
  52. data/src/lib/gloss/writer.gl +15 -13
  53. metadata +22 -7
data/lib/gloss/version.rb CHANGED
@@ -1,8 +1,8 @@
1
- # frozen_string_literal: true
1
+ # frozen_string_literal: true
2
2
 
3
- ##### This file was generated by Gloss; any changes made here will be overwritten.
4
- ##### See src/ to make changes
3
+ ##### This file was generated by Gloss; any changes made here will be overwritten.
4
+ ##### See src/ to make changes
5
5
 
6
6
  module Gloss
7
- VERSION = "0.0.5"
7
+ VERSION = "0.1.3"
8
8
  end
@@ -4,29 +4,24 @@
4
4
  ##### See src/ to make changes
5
5
 
6
6
  module Gloss
7
- module Utils
8
- module_function
9
- def with_file_header(str)
10
- "#{Builder::FILE_HEADER}\n\n#{str}"
11
- end
12
- end
13
- class Builder
7
+ class Visitor
14
8
  FILE_HEADER = " #{(if Config.frozen_string_literals
15
9
  "# frozen_string_literal: true\n"
16
10
  end)}\n ##### This file was generated by Gloss; any changes made here will be overwritten.\n ##### See #{Config.src_dir}/ to make changes"
17
- include Utils
18
11
  attr_reader(:"tree")
19
- def initialize(tree_hash, type_checker = nil)
12
+ def initialize(tree_hash, type_checker = nil, on_new_file_referenced = nil)
13
+ @on_new_file_referenced = on_new_file_referenced
20
14
  @indent_level = 0
21
15
  @inside_macro = false
22
16
  @eval_vars = false
23
17
  @current_scope = nil
24
18
  @tree = tree_hash
25
19
  @type_checker = type_checker
20
+ @after_module_function = false
26
21
  end
27
22
  def run()
28
23
  rb_output = visit_node(@tree)
29
- with_file_header(rb_output)
24
+ Utils.with_file_header(rb_output)
30
25
  end
31
26
  def visit_node(node, scope = Scope.new)
32
27
  src = Source.new(@indent_level)
@@ -40,21 +35,30 @@ case node.[](:"type")
40
35
  RBS::Namespace.root
41
36
  end)
42
37
  superclass_type = nil
43
- superclass_output = nil
38
+ superclass_output = ""
44
39
  (if node.[](:"superclass")
45
40
  @eval_vars = true
46
41
  superclass_output = visit_node(node.[](:"superclass"))
47
42
  @eval_vars = false
48
- superclass_type = RBS::Parser.parse_type(superclass_output)
43
+ args = Array.new
49
44
  (if node.dig(:"superclass", :"type")
50
45
  .==("Generic")
51
- superclass_output = superclass_output.[](/^[^\[]+/)
46
+ superclass_output = superclass_output.[](/^[^\[]+/) || superclass_output
47
+ args = node.dig(:"superclass", :"args")
48
+ .map() { |n|
49
+ RBS::Parser.parse_type(visit_node(n))
50
+ }
52
51
  end)
52
+ class_name_index = superclass_output.index(/[^(?:::)]+\z/) || 0
53
+ namespace = superclass_output.[](0, class_name_index)
54
+ superclass_name = superclass_output.[](/[^(?:::)]+\z/) || superclass_output
55
+ superclass_type = RBS::AST::Declarations::Class::Super.new(name: RBS::TypeName.new(namespace: method(:"Namespace")
56
+ .call(namespace), name: superclass_name.to_sym), args: args, location: build_location(node))
53
57
  end)
54
- src.write_ln("class #{class_name}#{(if superclass_output
58
+ src.write_ln("class #{class_name}#{unless superclass_output.blank?
55
59
  " < #{superclass_output}"
56
- end)}")
57
- class_type = RBS::AST::Declarations::Class.new(name: RBS::TypeName.new(namespace: current_namespace, name: class_name.to_sym), type_params: RBS::AST::Declarations::ModuleTypeParams.new, super_class: superclass_type, members: Array.new, annotations: Array.new, location: node.[](:"location"), comment: node.[](:"comment"))
60
+ end}")
61
+ class_type = RBS::AST::Declarations::Class.new(name: RBS::TypeName.new(namespace: current_namespace, name: class_name.to_sym), type_params: RBS::AST::Declarations::ModuleTypeParams.new, super_class: superclass_type, members: Array.new, annotations: Array.new, location: build_location(node), comment: node.[](:"comment"))
58
62
  old_parent_scope = @current_scope
59
63
  @current_scope = class_type
60
64
  indented(src) { ||
@@ -68,14 +72,13 @@ case node.[](:"type")
68
72
  @current_scope.members
69
73
  .<<(class_type)
70
74
  end)
71
- (if @type_checker
72
- unless @current_scope
73
- @type_checker.top_level_decls
74
- .[]=(class_type.name
75
- .name, class_type)
76
- end
75
+ (if @type_checker && !@current_scope
76
+ @type_checker.top_level_decls
77
+ .add(class_type)
77
78
  end)
78
79
  when "ModuleNode"
80
+ existing_module_function_state = @after_module_function.dup
81
+ @after_module_function = false
79
82
  module_name = visit_node(node.[](:"name"))
80
83
  src.write_ln("module #{module_name}")
81
84
  current_namespace = (if @current_scope
@@ -84,7 +87,7 @@ case node.[](:"type")
84
87
  else
85
88
  RBS::Namespace.root
86
89
  end)
87
- module_type = RBS::AST::Declarations::Module.new(name: RBS::TypeName.new(namespace: current_namespace, name: module_name.to_sym), type_params: RBS::AST::Declarations::ModuleTypeParams.new, self_types: Array.new, members: Array.new, annotations: Array.new, location: node.[](:"location"), comment: node.[](:"comment"))
90
+ module_type = RBS::AST::Declarations::Module.new(name: RBS::TypeName.new(namespace: current_namespace, name: module_name.to_sym), type_params: RBS::AST::Declarations::ModuleTypeParams.new, self_types: Array.new, members: Array.new, annotations: Array.new, location: build_location(node), comment: node.[](:"comment"))
88
91
  old_parent_scope = @current_scope
89
92
  @current_scope = module_type
90
93
  indented(src) { ||
@@ -97,31 +100,44 @@ case node.[](:"type")
97
100
  @current_scope.members
98
101
  .<<(module_type)
99
102
  end)
100
- (if @type_checker
101
- unless @current_scope
102
- @type_checker.top_level_decls
103
- .[]=(module_type.name
104
- .name, module_type)
105
- end
103
+ (if @type_checker && !@current_scope
104
+ @type_checker.top_level_decls
105
+ .add(module_type)
106
106
  end)
107
107
  src.write_ln("end")
108
+ @after_module_function = existing_module_function_state
108
109
  when "DefNode"
109
110
  args = render_args(node)
110
- src.write_ln("def #{node.[](:"name")}#{args.[](:"representation")}")
111
+ receiver = (if node.[](:"receiver")
112
+ visit_node(node.[](:"receiver"))
113
+ else
114
+ nil
115
+ end)
116
+ src.write_ln("def #{(if receiver
117
+ "#{receiver}."
118
+ end)}#{node.[](:"name")}#{args.[](:"representation")}")
111
119
  return_type = (if node.[](:"return_type")
112
120
  RBS::Types::ClassInstance.new(name: RBS::TypeName.new(name: eval(visit_node(node.[](:"return_type")))
113
121
  .to_s
114
- .to_sym, namespace: RBS::Namespace.root), args: EMPTY_ARRAY, location: node.[](:"location"))
122
+ .to_sym, namespace: RBS::Namespace.root), args: EMPTY_ARRAY, location: build_location(node))
115
123
  else
116
- RBS::Types::Bases::Any.new(location: node.[](:"location"))
124
+ RBS::Types::Bases::Any.new(location: build_location(node))
117
125
  end)
118
126
  method_types = [RBS::MethodType.new(type_params: EMPTY_ARRAY, type: RBS::Types::Function.new(required_positionals: args.dig(:"types", :"required_positionals"), optional_positionals: args.dig(:"types", :"optional_positionals"), rest_positionals: args.dig(:"types", :"rest_positionals"), trailing_positionals: args.dig(:"types", :"trailing_positionals"), required_keywords: args.dig(:"types", :"required_keywords"), optional_keywords: args.dig(:"types", :"optional_keywords"), rest_keywords: args.dig(:"types", :"rest_keywords"), return_type: return_type), block: (if node.[](:"yield_arg_count")
119
- RBS::Types::Block.new(type: RBS::Types::Function.new(required_positionals: Array.new, optional_positionals: Array.new, rest_positionals: nil, trailing_positionals: Array.new, required_keywords: Hash.new, optional_keywords: Hash.new, rest_keywords: nil, return_type: RBS::Types::Bases::Any.new(location: node.[](:"location"))), required: !!node.[](:"block_arg") || node.[](:"yield_arg_count"))
127
+ RBS::Types::Block.new(type: RBS::Types::Function.new(required_positionals: Array.new, optional_positionals: Array.new, rest_positionals: nil, trailing_positionals: Array.new, required_keywords: Hash.new, optional_keywords: Hash.new, rest_keywords: nil, return_type: RBS::Types::Bases::Any.new(location: build_location(node))), required: !!node.[](:"block_arg") || node.[](:"yield_arg_count"))
120
128
  else
121
129
  nil
122
- end), location: node.[](:"location"))]
130
+ end), location: build_location(node))]
123
131
  method_definition = RBS::AST::Members::MethodDefinition.new(name: node.[](:"name")
124
- .to_sym, kind: :"instance", types: method_types, annotations: EMPTY_ARRAY, location: node.[](:"location"), comment: node.[](:"comment"), overload: false)
132
+ .to_sym, kind: (if @after_module_function
133
+ :"singleton_instance"
134
+ else
135
+ (if receiver
136
+ :"singleton"
137
+ else
138
+ :"instance"
139
+ end)
140
+ end), types: method_types, annotations: EMPTY_ARRAY, location: build_location(node), comment: node.[](:"comment"), overload: false)
125
141
  (if @current_scope
126
142
  @current_scope.members
127
143
  .<<(method_definition)
@@ -179,9 +195,18 @@ EMPTY_ARRAY }
179
195
  else
180
196
  nil
181
197
  end)
182
- call = "#{obj}#{node.[](:"name")}#{opening_delimiter}#{args}#{(if has_parens
198
+ name = node.[](:"name")
199
+ call = "#{obj}#{name}#{opening_delimiter}#{args}#{(if has_parens
183
200
  ")"
184
201
  end)}#{block}"
202
+ case name
203
+ when "require_relative"
204
+ (if @on_new_file_referenced
205
+ @on_new_file_referenced.call(name, true)
206
+ end)
207
+ when "module_function"
208
+ @after_module_function = true
209
+ end
185
210
  src.write_ln(call)
186
211
  when "Block"
187
212
  args = render_args(node)
@@ -197,7 +222,7 @@ EMPTY_ARRAY }
197
222
  else
198
223
  ".."
199
224
  end)
200
- src.write("(", visit_node(node.[](:"from")), dots, visit_node(node.[](:"to")), ")")
225
+ src.write("(", "(", visit_node(node.[](:"from")), ")", dots, "(", visit_node(node.[](:"to")), ")", ")")
201
226
  when "LiteralNode"
202
227
  src.write(node.[](:"value"))
203
228
  when "ArrayLiteral"
@@ -216,7 +241,7 @@ EMPTY_ARRAY }
216
241
  str.<<(case c.[](:"type")
217
242
  when "LiteralNode"
218
243
  c.[](:"value")
219
- .[]((1...-1))
244
+ .[](((1)...(-1)))
220
245
  else
221
246
  ["\#{", visit_node(c)
222
247
  .strip, "}"].join
@@ -226,7 +251,11 @@ EMPTY_ARRAY }
226
251
  when "Path"
227
252
  src.write(node.[](:"value"))
228
253
  when "Require"
229
- src.write_ln("require \"#{node.[](:"value")}\"")
254
+ path = node.[](:"value")
255
+ src.write_ln("require \"#{path}\"")
256
+ (if @on_new_file_referenced
257
+ @on_new_file_referenced.call(path, false)
258
+ end)
230
259
  when "Assign", "OpAssign"
231
260
  src.write_ln("#{visit_node(node.[](:"target"))} #{node.[](:"op")}= #{visit_node(node.[](:"value"))
232
261
  .strip}")
@@ -278,11 +307,12 @@ EMPTY_ARRAY }
278
307
  key = case k
279
308
  when String
280
309
  k.to_sym
310
+ .inspect
281
311
  else
282
312
  visit_node(k)
283
313
  end
284
314
  value = visit_node(v)
285
- "#{key.inspect} => #{value}" }
315
+ "#{key} => #{value}" }
286
316
  src.write("{#{contents.join(",\n")}}")
287
317
  (if node.[](:"frozen")
288
318
  src.write(".freeze")
@@ -414,7 +444,12 @@ EMPTY_ARRAY }
414
444
  src.write("return#{val}")
415
445
  when "TypeDeclaration"
416
446
  src.write_ln("# @type var #{visit_node(node.[](:"var"))}: #{visit_node(node.[](:"declared_type"))}")
417
- src.write_ln("#{visit_node(node.[](:"var"))} = #{visit_node(node.[](:"value"))}")
447
+ value = (if node.[](:"value")
448
+ " = #{visit_node(node.[](:"value"))}"
449
+ else
450
+ nil
451
+ end)
452
+ src.write_ln("#{visit_node(node.[](:"var"))}#{value}")
418
453
  when "ExceptionHandler"
419
454
  src.write_ln("begin")
420
455
  indented(src) { ||
@@ -471,7 +506,7 @@ EMPTY_ARRAY }
471
506
  name = visit_node(node.[](:"name"))
472
507
  src.write_ln("include #{name}")
473
508
  type = RBS::AST::Members::Include.new(name: method(:"TypeName")
474
- .call(name), args: Array.new, annotations: Array.new, location: node.[](:"location"), comment: node.[](:"comment"))
509
+ .call(name), args: Array.new, annotations: Array.new, location: build_location(node), comment: node.[](:"comment"))
475
510
  (if @current_scope
476
511
  @current_scope.members
477
512
  .<<(type)
@@ -489,7 +524,7 @@ EMPTY_ARRAY }
489
524
  name = visit_node(node.[](:"name"))
490
525
  src.write_ln("extend #{name}")
491
526
  type = RBS::AST::Members::Extend.new(name: method(:"TypeName")
492
- .call(name), args: Array.new, annotations: Array.new, location: node.[](:"location"), comment: node.[](:"comment"))
527
+ .call(name), args: Array.new, annotations: Array.new, location: build_location(node), comment: node.[](:"comment"))
493
528
  (if @current_scope
494
529
  @current_scope.members
495
530
  .<<(type)
@@ -595,34 +630,38 @@ a && a.empty? }
595
630
  .flatten
596
631
  .join(", ")
597
632
  representation = "(#{contents})"
598
- rp.map!() { |a|
633
+ rp_args = rp.map() { |a|
599
634
  RBS::Types::Function::Param.new(name: visit_node(a)
600
- .to_sym, type: RBS::Types::Bases::Any.new(location: a.[](:"location")))
635
+ .to_sym, type: RBS::Types::Bases::Any.new(location: build_location(a)))
601
636
  }
602
- op.map!() { |a|
637
+ op_args = op.map() { |a|
603
638
  RBS::Types::Function::Param.new(name: visit_node(a)
604
- .to_sym, type: RBS::Types::Bases::Any.new(location: a.[](:"location")))
639
+ .to_sym, type: RBS::Types::Bases::Any.new(location: build_location(a)))
605
640
  }
606
- rest_p = (if rpa = node.[](:"rest_p_args")
607
- RBS::Types::Function::Param.new(name: visit_node(rpa)
608
- .to_sym, type: RBS::Types::Bases::Any.new(location: node.[](:"location")))
641
+ rpa = (if rest_p
642
+ RBS::Types::Function::Param.new(name: rest_p.to_sym, type: RBS::Types::Bases::Any.new(location: build_location(node)))
609
643
  else
610
644
  nil
611
645
  end)
612
646
  {:representation => representation,
613
- :types => {:required_positionals => rp,
614
- :optional_positionals => op,
615
- :rest_positionals => rest_p,
647
+ :types => {:required_positionals => rp_args,
648
+ :optional_positionals => op_args,
649
+ :rest_positionals => rpa,
616
650
  :trailing_positionals => EMPTY_ARRAY,
617
651
  :required_keywords => node.[](:"req_kw_args") || EMPTY_HASH,
618
652
  :optional_keywords => node.[](:"opt_kw_args") || EMPTY_HASH,
619
653
  :rest_keywords => (if node.[](:"rest_kw_args")
620
654
  RBS::Types::Function::Param.new(name: visit_node(node.[](:"rest_kw_args"))
621
- .to_sym, type: RBS::Types::Bases::Any.new(location: node.[](:"location")))
655
+ .to_sym, type: RBS::Types::Bases::Any.new(location: build_location(node)))
622
656
  else
623
657
  nil
624
658
  end)
625
659
  }.freeze}.freeze
626
660
  end
661
+ def build_location(node)
662
+ unless node.[](:"location")
663
+ return nil
664
+ end
665
+ end
627
666
  end
628
667
  end
data/lib/gloss/watcher.rb CHANGED
@@ -3,50 +3,72 @@
3
3
  ##### This file was generated by Gloss; any changes made here will be overwritten.
4
4
  ##### See src/ to make changes
5
5
 
6
- require "listen"
6
+ require "listen"
7
7
  module Gloss
8
8
  class Watcher
9
9
  def initialize(paths)
10
10
  @paths = paths
11
11
  (if @paths.empty?
12
12
  @paths = [File.join(Dir.pwd, Config.src_dir)]
13
+ @only = /(?:(\.gl|(?:(?<=\/)[^\.\/]+))\z|\A[^\.\/]+\z)/
14
+ else
15
+ file_names = Array.new
16
+ paths = Array.new
17
+ @paths.each() { |pa|
18
+ pn = Pathname.new(pa)
19
+ paths.<<(pn.parent
20
+ .to_s)
21
+ file_names.<<((if pn.file?
22
+ pn.basename
23
+ .to_s
24
+ else
25
+ pa
26
+ end))
27
+ }
28
+ @paths = paths.uniq
29
+ @only = /#{Regexp.union(file_names)}/
13
30
  end)
14
31
  end
15
32
  def watch()
16
- puts("=====> Now listening for changes in #{@paths.join(", ")}")
17
- listener = Listen.to(*@paths, latency: 2) { |modified, added, removed|
33
+ Gloss.logger
34
+ .info("Now listening for changes in #{@paths.join(", ")}")
35
+ listener = Listen.to(*@paths, latency: 2, only: @only) { |modified, added, removed|
18
36
  modified.+(added)
19
37
  .each() { |f|
20
- unless f.end_with?(".gl")
21
- next
22
- end
23
- puts("====> Rewriting #{f}")
38
+ Gloss.logger
39
+ .info("Rewriting #{f}")
24
40
  content = File.read(f)
25
- Writer.new(Builder.new(Parser.new(content)
41
+ err = catch(:"error") { ||
42
+ Writer.new(Visitor.new(Parser.new(content)
26
43
  .run)
27
44
  .run, f)
28
45
  .run
29
- puts("====> Done")
46
+ nil }
47
+ (if err
48
+ Gloss.logger
49
+ .error(err)
50
+ else
51
+ Gloss.logger
52
+ .info("Done")
53
+ end)
30
54
  }
31
55
  removed.each() { |f|
32
- unless f.end_with?(".gl")
33
- next
34
- end
35
56
  out_path = Utils.src_path_to_output_path(f)
36
- puts("====> Removing #{out_path}")
57
+ Gloss.logger
58
+ .info("Removing #{out_path}")
37
59
  (if File.exist?(out_path)
38
60
  File.delete(out_path)
39
61
  end)
40
- puts("====> Done")
62
+ Gloss.logger
63
+ .info("Done")
41
64
  }
42
65
  }
43
- listener.start
44
66
  begin
45
- loop() { ||
46
- sleep(10)
47
- }
67
+ listener.start
68
+ sleep
48
69
  rescue Interrupt
49
- puts("=====> Interrupt signal received, shutting down")
70
+ Gloss.logger
71
+ .info("Interrupt signal received, shutting down")
50
72
  exit(0)
51
73
  end
52
74
  end
data/lib/gloss/writer.rb CHANGED
@@ -3,20 +3,13 @@
3
3
  ##### This file was generated by Gloss; any changes made here will be overwritten.
4
4
  ##### See src/ to make changes
5
5
 
6
- require "pathname"
6
+ require "pathname"
7
7
  require "fileutils"
8
8
  module Gloss
9
- module Utils
10
- module_function
11
- def src_path_to_output_path(src_path)
12
- src_path.sub("#{Config.src_dir}/", "")
13
- .sub(/\.gl$/, ".rb")
14
- end
15
- end
16
9
  class Writer
17
- include Utils
18
- def initialize(content, src_path, output_path = Pathname.new(src_path_to_output_path(src_path)))
10
+ def initialize(content, src_path, output_path = Pathname.new(Utils.src_path_to_output_path(src_path)))
19
11
  @content = content
12
+ @src_path = src_path
20
13
  @output_path = output_path
21
14
  end
22
15
  def run()
@@ -25,8 +18,26 @@ module Gloss
25
18
  FileUtils.mkdir_p(@output_path.parent)
26
19
  end
27
20
  File.open(@output_path, "wb") { |file|
21
+ sb = shebang
22
+ (if sb
23
+ file.puts(sb)
24
+ end)
28
25
  file.puts(@content)
29
26
  }
30
27
  end
28
+ private def shebang()
29
+ (if @output_path.executable?
30
+ first_line = File.open(@src_path) { |f|
31
+ f.readline
32
+ }
33
+ (if first_line.start_with?("#!")
34
+ first_line
35
+ else
36
+ nil
37
+ end)
38
+ else
39
+ nil
40
+ end)
41
+ end
31
42
  end
32
43
  end