gloss 0.0.6 → 0.1.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/tests.yml +41 -0
  3. data/.gloss.yml +2 -0
  4. data/Gemfile.lock +10 -10
  5. data/README.md +5 -7
  6. data/Rakefile +5 -3
  7. data/exe/gloss +10 -13
  8. data/ext/gloss/Makefile +6 -21
  9. data/ext/gloss/lib/cr_ruby.cr +5 -4
  10. data/ext/gloss/src/cr_ast.cr +61 -71
  11. data/ext/gloss/src/gloss.cr +7 -2
  12. data/ext/gloss/src/rb_ast.cr +37 -36
  13. data/gloss.gemspec +3 -3
  14. data/lib/gloss.rb +10 -7
  15. data/lib/gloss/cli.rb +61 -40
  16. data/lib/gloss/config.rb +14 -8
  17. data/lib/gloss/errors.rb +1 -1
  18. data/lib/gloss/logger.rb +6 -6
  19. data/lib/gloss/prog_loader.rb +144 -0
  20. data/lib/gloss/scope.rb +1 -1
  21. data/lib/gloss/source.rb +1 -1
  22. data/lib/gloss/type_checker.rb +60 -32
  23. data/lib/gloss/utils.rb +38 -0
  24. data/lib/gloss/version.rb +1 -1
  25. data/lib/gloss/{builder.rb → visitor.rb} +88 -53
  26. data/lib/gloss/watcher.rb +14 -7
  27. data/lib/gloss/writer.rb +21 -10
  28. data/logo.svg +6 -0
  29. data/sig/core.rbs +2 -0
  30. data/sig/fast_blank.rbs +4 -0
  31. data/sig/{gloss.rbs → gls.rbs} +0 -0
  32. data/sig/optparse.rbs +6 -0
  33. data/sig/rubygems.rbs +9 -0
  34. data/sig/yaml.rbs +3 -0
  35. data/src/exe/gloss +19 -0
  36. data/src/lib/gloss.gl +25 -0
  37. data/src/lib/gloss/cli.gl +41 -26
  38. data/src/lib/gloss/config.gl +13 -8
  39. data/src/lib/gloss/logger.gl +2 -5
  40. data/src/lib/gloss/prog_loader.gl +138 -0
  41. data/src/lib/gloss/scope.gl +0 -2
  42. data/src/lib/gloss/type_checker.gl +57 -28
  43. data/src/lib/gloss/utils.gl +35 -0
  44. data/src/lib/gloss/version.gl +1 -1
  45. data/src/lib/gloss/{builder.gl → visitor.gl} +82 -45
  46. data/src/lib/gloss/watcher.gl +13 -8
  47. data/src/lib/gloss/writer.gl +15 -13
  48. metadata +29 -18
  49. data/.github/workflows/crystal_specs.yml +0 -26
  50. data/.github/workflows/ruby_specs.yml +0 -33
@@ -1,5 +1,3 @@
1
- # frozen_string_literal: true
2
-
3
1
  module Gloss
4
2
  class Scope < Hash[String, String]
5
3
  def [](k)
@@ -1,28 +1,45 @@
1
- # frozen_string_literal: true
2
- require "pry-byebug"
1
+ require "set"
3
2
 
4
3
  module Gloss
5
4
  class TypeChecker
6
- Project = Struct.new :targets
5
+ attr_reader :steep_target, :top_level_decls, :env, :rbs_gem_dir
7
6
 
8
- attr_reader :steep_target, :top_level_decls
7
+ enum Strictness
8
+ Strict = "strict"
9
+ Lenient = "lenient"
10
+ Default = "default"
11
+ end
9
12
 
10
13
  def initialize
14
+ options = Steep::Project::Options.new
15
+ case Config.type_checking_strictness
16
+ when Strictness::Strict
17
+ options.apply_strict_typing_options!
18
+ when Strictness::Lenient
19
+ options.apply_lenient_typing_options!
20
+ else
21
+ options.apply_default_typing_options!
22
+ end
11
23
  @steep_target = Steep::Project::Target.new(
12
24
  name: "gloss",
13
- options: Steep::Project::Options.new.tap do |o|
14
- o.allow_unknown_constant_assignment = true
15
- end,
16
- source_patterns: ["gloss.rb"],
25
+ options: options,
26
+ source_patterns: ["**/*.rb"],
17
27
  ignore_patterns: Array.new,
18
28
  signature_patterns: ["sig"]
19
29
  )
20
- @top_level_decls = {}
30
+ @top_level_decls = Set.new
31
+ @rbs_gem_dir = Utils.gem_path_for("rbs")
32
+ env_loader = RBS::EnvironmentLoader.new
33
+ @env = RBS::Environment.from_loader(env_loader)
34
+ project = Steep::Project.new(steepfile_path: Pathname.new(Config.src_dir).realpath)
35
+ project.targets << @steep_target
36
+ loader = Steep::Project::FileLoader.new(project: project)
37
+ #loader.load_signatures
21
38
  end
22
39
 
23
- def run(rb_str)
40
+ def run(filepath, rb_str)
24
41
  begin
25
- valid_types = check_types rb_str
42
+ valid_types = check_types filepath, rb_str
26
43
  rescue ParseError => e
27
44
  throw :error, ""
28
45
  rescue => e
@@ -38,8 +55,8 @@ module Gloss
38
55
  "Invalid method body type - expected: #{e.expected}, actual: #{e.actual}"
39
56
  when Steep::Diagnostic::Ruby::IncompatibleArguments
40
57
  <<-ERR
41
- Invalid argmuents - method type: #{e.method_type}
42
- method name: #{e.method_type.method_decls.first.method_name}
58
+ Invalid argmuents - method type: #{e.method_types.first}
59
+ method name: #{e.method_name}
43
60
  ERR
44
61
  when Steep::Diagnostic::Ruby::ReturnTypeMismatch
45
62
  "Invalid return type - expected: #{e.expected}, actual: #{e.actual}"
@@ -48,7 +65,7 @@ module Gloss
48
65
  when Steep::Diagnostic::Ruby::UnexpectedBlockGiven
49
66
  "Unexpected block given"
50
67
  else
51
- e.inspect
68
+ "#{e.header_line}\n#{e}"
52
69
  end
53
70
  }.join("\n")
54
71
  throw :error, errors
@@ -57,31 +74,38 @@ module Gloss
57
74
  true
58
75
  end
59
76
 
60
- def check_types(rb_str)
61
- env_loader = RBS::EnvironmentLoader.new
62
- env = RBS::Environment.from_loader(env_loader)
63
- project = Steep::Project.new(steepfile_path: Pathname.new(Config.src_dir).realpath)
64
- project.targets << @steep_target
65
- loader = Steep::Project::FileLoader.new(project: project)
66
- loader.load_signatures
77
+ def ready_for_checking!
78
+ @top_level_decls.each do |decl|
79
+ @env << decl
80
+ end
81
+ @env = @env.resolve_type_names
67
82
 
68
- @steep_target.add_source("gloss.rb", rb_str)
83
+ @steep_target.instance_variable_set("@environment", @env)
84
+ end
69
85
 
70
- @top_level_decls.each do |_, decl|
71
- env << decl
72
- end
73
- env = env.resolve_type_names
86
+ def check_types(filepath, rb_str)
87
+ @steep_target.add_source(filepath, rb_str)
74
88
 
75
- @steep_target.instance_variable_set("@environment", env)
89
+ ready_for_checking!
76
90
 
77
91
  @steep_target.type_check
78
92
 
79
93
  if @steep_target.status.is_a? Steep::Project::Target::SignatureErrorStatus
80
94
  throw :error, @steep_target.status.errors.map { |e|
95
+ msg = case e
96
+ when Steep::Diagnostic::Signature::UnknownTypeName
97
+ "Unknown type name: #{e.name.name} (#{e.location.source[/^.*$/]})"
98
+ when Steep::Diagnostic::Signature::InvalidTypeApplication
99
+ "Invalid type application: #{e.header_line}"
100
+ when Steep::Diagnostic::Signature::DuplicatedMethodDefinition
101
+ "Duplicated method: #{e.header_line}"
102
+ else
103
+ e.header_line
104
+ end
81
105
  <<~MSG
82
106
  SignatureSyntaxError:
83
107
  Location: #{e.location}
84
- Message: "#{e.exception.error_value.value}"
108
+ Message: "#{msg}"
85
109
  MSG
86
110
  }.join("\n")
87
111
  end
@@ -97,5 +121,10 @@ module Gloss
97
121
  @steep_target.no_error? &&
98
122
  @steep_target.errors.empty?
99
123
  end
124
+
125
+ def load_sig_path(path : String)
126
+ Gloss.logger.debug "Loading signature file for #{path}"
127
+ @steep_target.add_signature path, File.open(path).read
128
+ end
100
129
  end
101
130
  end
@@ -0,0 +1,35 @@
1
+ require "rubygems"
2
+
3
+ module Gloss
4
+ module Utils
5
+ module_function
6
+
7
+ def absolute_path(path)
8
+ pn = Pathname.new(path)
9
+ if pn.absolute?
10
+ pn.to_s
11
+ else
12
+ ap = File.absolute_path path
13
+ if File.exist? ap
14
+ ap
15
+ else
16
+ throw :error, "File path #{path} does not exist (also looked for #{ap})"
17
+ end
18
+ end
19
+ end
20
+
21
+ def gem_path_for(gem_name)
22
+ spec = Gem::Specification.find_by_path(gem_name)
23
+ spec.full_require_paths.first if spec
24
+ end
25
+
26
+ def with_file_header(str)
27
+ "#{Visitor::FILE_HEADER}\n\n#{str}"
28
+ end
29
+
30
+ def src_path_to_output_path(src_path : String) : String
31
+ src_path.sub("#{Config.src_dir}/", "")
32
+ .sub(/\.gl$/, ".rb")
33
+ end
34
+ end
35
+ end
@@ -1,3 +1,3 @@
1
1
  module Gloss
2
- VERSION = "0.0.6"
2
+ VERSION = "0.1.4"
3
3
  end
@@ -1,35 +1,26 @@
1
1
  module Gloss
2
- module Utils
3
- module_function
4
-
5
- def with_file_header(str)
6
- "#{Builder::FILE_HEADER}\n\n#{str}"
7
- end
8
- end
9
-
10
- class Builder
2
+ class Visitor
11
3
  FILE_HEADER = <<~RUBY
12
4
  #{"# frozen_string_literal: true\n" if Config.frozen_string_literals}
13
5
  ##### This file was generated by Gloss; any changes made here will be overwritten.
14
6
  ##### See #{Config.src_dir}/ to make changes
15
7
  RUBY
16
8
 
17
- include Utils
18
-
19
9
  attr_reader :tree
20
10
 
21
- def initialize(tree_hash, type_checker = nil)
11
+ def initialize(tree_hash, type_checker = nil, @on_new_file_referenced = nil)
22
12
  @indent_level = 0
23
13
  @inside_macro = false
24
14
  @eval_vars = false
25
15
  @current_scope = nil
26
16
  @tree = tree_hash
27
17
  @type_checker = type_checker
18
+ @after_module_function = false
28
19
  end
29
20
 
30
21
  def run
31
22
  rb_output = visit_node(@tree)
32
- with_file_header(rb_output)
23
+ Utils.with_file_header(rb_output)
33
24
  end
34
25
 
35
26
  # type node = Hash[Symbol, String | Array[String | node] | Hash[Symbol, node]] | true | false
@@ -41,18 +32,31 @@ module Gloss
41
32
  class_name = visit_node(node[:name])
42
33
  current_namespace = @current_scope ? @current_scope.name.to_namespace : RBS::Namespace.root
43
34
  superclass_type = nil
44
- superclass_output = nil
35
+ superclass_output = ""
45
36
  if node[:superclass]
46
37
  @eval_vars = true
47
38
  superclass_output = visit_node(node[:superclass])
48
39
  @eval_vars = false
49
- superclass_type = RBS::Parser.parse_type superclass_output
40
+ args = Array.new
50
41
  if node.dig(:superclass, :type) == "Generic"
51
- superclass_output = superclass_output[/^[^\[]+/]
42
+ superclass_output = superclass_output[/^[^\[]+/] || superclass_output
43
+ args = node.dig(:superclass, :args).map { |n| RBS::Parser.parse_type(visit_node(n)) }
52
44
  end
45
+
46
+ class_name_index = superclass_output.index(/[^(?:::)]+\z/) || 0
47
+ namespace = superclass_output[0, class_name_index]
48
+ superclass_name = superclass_output[/[^(?:::)]+\z/] || superclass_output
49
+ superclass_type = RBS::AST::Declarations::Class::Super.new(
50
+ name: RBS::TypeName.new(
51
+ namespace: method(:Namespace).call(namespace),
52
+ name: superclass_name.to_sym,
53
+ ),
54
+ args: args,
55
+ location: build_location(node),
56
+ )
53
57
  end
54
58
 
55
- src.write_ln "class #{class_name}#{" < #{superclass_output}" if superclass_output}"
59
+ src.write_ln "class #{class_name}#{" < #{superclass_output}" unless superclass_output.blank?}"
56
60
 
57
61
  class_type = RBS::AST::Declarations::Class.new(
58
62
  name: RBS::TypeName.new(
@@ -63,7 +67,7 @@ module Gloss
63
67
  super_class: superclass_type,
64
68
  members: Array.new, # TODO
65
69
  annotations: Array.new, # TODO
66
- location: node[:location],
70
+ location: build_location(node),
67
71
  comment: node[:comment]
68
72
  )
69
73
  old_parent_scope = @current_scope
@@ -77,10 +81,12 @@ module Gloss
77
81
 
78
82
  @current_scope.members << class_type if @current_scope
79
83
 
80
- if @type_checker
81
- @type_checker.top_level_decls[class_type.name.name] = class_type unless @current_scope
84
+ if @type_checker && !@current_scope
85
+ @type_checker.top_level_decls.add(class_type)
82
86
  end
83
87
  when "ModuleNode"
88
+ existing_module_function_state = @after_module_function.dup
89
+ @after_module_function = false
84
90
  module_name = visit_node node[:name]
85
91
  src.write_ln "module #{module_name}"
86
92
 
@@ -95,8 +101,8 @@ module Gloss
95
101
  self_types: Array.new, # TODO
96
102
  members: Array.new, # TODO
97
103
  annotations: Array.new, # TODO
98
- location: node[:location],
99
- comment: node[:comment]
104
+ location: build_location(node),
105
+ comment: node[:comment],
100
106
  )
101
107
  old_parent_scope = @current_scope
102
108
  @current_scope = module_type
@@ -107,10 +113,11 @@ module Gloss
107
113
 
108
114
  @current_scope.members << module_type if @current_scope
109
115
 
110
- if @type_checker
111
- @type_checker.top_level_decls[module_type.name.name] = module_type unless @current_scope
116
+ if @type_checker && !@current_scope
117
+ @type_checker.top_level_decls.add(module_type)
112
118
  end
113
119
  src.write_ln "end"
120
+ @after_module_function = existing_module_function_state
114
121
  when "DefNode"
115
122
  args = render_args(node)
116
123
  receiver = node[:receiver] ? visit_node(node[:receiver]) : nil
@@ -123,11 +130,11 @@ module Gloss
123
130
  namespace: RBS::Namespace.root
124
131
  ),
125
132
  args: EMPTY_ARRAY, # TODO
126
- location: node[:location]
133
+ location: build_location(node)
127
134
  )
128
135
  else
129
136
  RBS::Types::Bases::Any.new(
130
- location: node[:location]
137
+ location: build_location(node)
131
138
  )
132
139
  end
133
140
 
@@ -154,19 +161,19 @@ module Gloss
154
161
  required_keywords: Hash.new,
155
162
  optional_keywords: Hash.new,
156
163
  rest_keywords: nil,
157
- return_type: RBS::Types::Bases::Any.new(location: node[:location])
164
+ return_type: RBS::Types::Bases::Any.new(location: build_location(node))
158
165
  ),
159
166
  required: !!(node[:block_arg] || node[:yield_arg_count])
160
167
  ) : nil,
161
- location: node[:location]
168
+ location: build_location(node)
162
169
  )
163
170
  ]
164
171
  method_definition = RBS::AST::Members::MethodDefinition.new(
165
172
  name: node[:name].to_sym,
166
- kind: receiver ? :class : :instance,
173
+ kind: @after_module_function ? :singleton_instance : receiver ? :singleton : :instance,
167
174
  types: method_types,
168
175
  annotations: EMPTY_ARRAY, # TODO
169
- location: node[:location],
176
+ location: build_location(node),
170
177
  comment: node[:comment],
171
178
  overload: false
172
179
  )
@@ -195,6 +202,7 @@ module Gloss
195
202
  else
196
203
  nil
197
204
  end
205
+ name = node[:name]
198
206
  block = node[:block] ? " #{visit_node(node[:block])}" : nil
199
207
  has_parens = !!(node[:has_parentheses] || args || block)
200
208
  opening_delimiter = if has_parens
@@ -202,7 +210,21 @@ module Gloss
202
210
  else
203
211
  nil
204
212
  end
205
- call = "#{obj}#{node[:name]}#{opening_delimiter}#{args}#{")" if has_parens}#{block}"
213
+ call = "#{obj}#{name}#{opening_delimiter}#{args}#{")" if has_parens}#{block}"
214
+ case name
215
+ when "require_relative"
216
+ if @on_new_file_referenced
217
+ paths = arg_arr.map do |a|
218
+ unless a[:type] == "LiteralNode"
219
+ throw :error, "Dynamic file paths are not allowed in require_relative"
220
+ end
221
+ eval(visit_node(a, scope).strip)
222
+ end
223
+ @on_new_file_referenced.call(paths, true)
224
+ end
225
+ when "module_function"
226
+ @after_module_function = true
227
+ end
206
228
  src.write_ln(call)
207
229
 
208
230
  when "Block"
@@ -247,9 +269,10 @@ module Gloss
247
269
  src.write node[:value]
248
270
 
249
271
  when "Require"
272
+ path = node[:value]
273
+ src.write_ln %(require "#{path}")
250
274
 
251
- src.write_ln %(require "#{node[:value]}")
252
-
275
+ @on_new_file_referenced.call([path], false) if @on_new_file_referenced
253
276
  when "Assign", "OpAssign"
254
277
 
255
278
  src.write_ln "#{visit_node(node[:target])} #{node[:op]}= #{visit_node(node[:value]).strip}"
@@ -388,7 +411,8 @@ module Gloss
388
411
  src.write "return#{val}"
389
412
  when "TypeDeclaration"
390
413
  src.write_ln "# @type var #{visit_node(node[:var])}: #{visit_node(node[:declared_type])}"
391
- src.write_ln "#{visit_node(node[:var])} = #{visit_node(node[:value])}"
414
+ value = node[:value] ? " = #{visit_node node[:value]}" : nil
415
+ src.write_ln "#{visit_node(node[:var])}#{value}"
392
416
  when "ExceptionHandler"
393
417
  src.write_ln "begin"
394
418
  indented src do
@@ -422,7 +446,7 @@ module Gloss
422
446
  name: method(:TypeName).call(name),
423
447
  args: Array.new,
424
448
  annotations: Array.new,
425
- location: node[:location],
449
+ location: build_location(node),
426
450
  comment: node[:comment]
427
451
  )
428
452
  if @current_scope
@@ -438,7 +462,7 @@ module Gloss
438
462
  name: method(:TypeName).call(name),
439
463
  args: Array.new,
440
464
  annotations: Array.new,
441
- location: node[:location],
465
+ location: build_location(node),
442
466
  comment: node[:comment]
443
467
  )
444
468
  if @current_scope
@@ -510,37 +534,50 @@ module Gloss
510
534
  rest_kw ? "**#{visit_node(rest_kw)}" : ""
511
535
  ].reject(&:empty?).flatten.join(", ")
512
536
  representation = "(#{contents})"
513
- rp.map! do |a|
537
+ rp_args = rp.map do |a|
514
538
  RBS::Types::Function::Param.new(
515
539
  name: visit_node(a).to_sym,
516
540
  type: RBS::Types::Bases::Any.new(
517
- location: a[:location]
541
+ location: build_location(a)
518
542
  )
519
543
  )
520
544
  end
521
- op.map! do |a|
545
+ op_args = op.map do |a|
522
546
  RBS::Types::Function::Param.new(
523
547
  name: visit_node(a).to_sym,
524
- type: RBS::Types::Bases::Any.new(location: a[:location])
548
+ type: RBS::Types::Bases::Any.new(location: build_location(a))
525
549
  )
526
550
  end
527
- rest_p = (rpa = node[:rest_p_args]) ? RBS::Types::Function::Param.new(name: visit_node(rpa).to_sym, type: RBS::Types::Bases::Any.new(location: node[:location])) : nil
551
+ rpa = rest_p ? RBS::Types::Function::Param.new(
552
+ name: rest_p.to_sym,
553
+ type: RBS::Types::Bases::Any.new(location: build_location(node))
554
+ ) : nil
528
555
  {
529
556
  representation: representation,
530
557
  types: {
531
- required_positionals: rp,
532
- optional_positionals: op,
533
- rest_positionals: rest_p,
558
+ required_positionals: rp_args,
559
+ optional_positionals: op_args,
560
+ rest_positionals: rpa,
534
561
  trailing_positionals: EMPTY_ARRAY, # TODO
535
562
  required_keywords: node[:req_kw_args] || EMPTY_HASH,
536
563
  optional_keywords: node[:opt_kw_args] || EMPTY_HASH,
537
564
  rest_keywords: node[:rest_kw_args] ?
538
565
  RBS::Types::Function::Param.new(
539
566
  name: visit_node(node[:rest_kw_args]).to_sym,
540
- type: RBS::Types::Bases::Any.new(location: node[:location])
567
+ type: RBS::Types::Bases::Any.new(location: build_location(node))
541
568
  ) : nil
542
569
  }
543
570
  }
544
571
  end
572
+
573
+ def build_location(node)
574
+ return nil unless node[:location]
575
+
576
+ # RBS::Location.new(
577
+ # buffer: RBS::Buffer.new(@file_path, @file_content),
578
+ # start_pos: node[:location][:start],
579
+ # end_pos: node[:location][:end]
580
+ # )
581
+ end
545
582
  end
546
583
  end