gloss 0.0.6 → 0.1.4

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