gloss 0.0.5 → 0.1.3

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 (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