gloss 0.0.1 → 0.0.6

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 (57) hide show
  1. checksums.yaml +4 -4
  2. data/.gitattributes +3 -0
  3. data/.github/workflows/crystal_specs.yml +26 -0
  4. data/.github/workflows/ruby_specs.yml +33 -0
  5. data/.gitignore +2 -1
  6. data/.rspec +1 -0
  7. data/Gemfile +0 -1
  8. data/Gemfile.lock +26 -34
  9. data/LICENSE +21 -0
  10. data/README.md +59 -7
  11. data/Rakefile +4 -0
  12. data/exe/gloss +15 -1
  13. data/ext/gloss/Makefile +27 -5
  14. data/ext/gloss/extconf.rb +3 -25
  15. data/ext/gloss/{src/lib → lib}/cr_ruby.cr +0 -0
  16. data/ext/gloss/lib/rbs_types.cr +3 -0
  17. data/ext/gloss/spec/parser_spec.cr +124 -0
  18. data/ext/gloss/spec/spec_helper.cr +2 -0
  19. data/ext/gloss/src/cr_ast.cr +129 -31
  20. data/ext/gloss/src/gloss.cr +12 -18
  21. data/ext/gloss/src/lexer.cr +59 -1
  22. data/ext/gloss/src/parser.cr +548 -254
  23. data/ext/gloss/src/rb_ast.cr +153 -27
  24. data/gloss.gemspec +1 -0
  25. data/lib/gloss.rb +4 -1
  26. data/lib/gloss/builder.rb +607 -409
  27. data/lib/gloss/cli.rb +64 -24
  28. data/lib/gloss/config.rb +16 -10
  29. data/lib/gloss/errors.rb +13 -7
  30. data/lib/gloss/initializer.rb +11 -6
  31. data/lib/gloss/logger.rb +34 -0
  32. data/lib/gloss/parser.rb +35 -0
  33. data/lib/gloss/scope.rb +8 -3
  34. data/lib/gloss/source.rb +18 -15
  35. data/lib/gloss/type_checker.rb +96 -0
  36. data/lib/gloss/version.rb +6 -1
  37. data/lib/gloss/watcher.rb +63 -19
  38. data/lib/gloss/writer.rb +18 -12
  39. data/sig/gloss.rbs +3 -0
  40. data/sig/listen.rbs +1 -0
  41. data/src/lib/gloss/builder.gl +546 -0
  42. data/src/lib/gloss/cli.gl +55 -0
  43. data/src/lib/gloss/config.gl +21 -0
  44. data/src/lib/gloss/errors.gl +11 -0
  45. data/src/lib/gloss/initializer.gl +20 -0
  46. data/src/lib/gloss/logger.gl +26 -0
  47. data/src/lib/gloss/parser.gl +31 -0
  48. data/src/lib/gloss/scope.gl +9 -0
  49. data/src/lib/gloss/source.gl +32 -0
  50. data/src/lib/gloss/type_checker.gl +101 -0
  51. data/src/lib/gloss/version.gl +3 -0
  52. data/src/lib/gloss/watcher.gl +67 -0
  53. data/src/lib/gloss/writer.gl +33 -0
  54. metadata +42 -6
  55. data/lib/gloss.bundle.dwarf +0 -0
  56. data/src/lib/hrb/initializer.gl +0 -22
  57. data/src/lib/hrb/watcher.gl +0 -32
@@ -0,0 +1,55 @@
1
+ require "optparse"
2
+
3
+ module Gloss
4
+ class CLI
5
+ def initialize(argv)
6
+ @argv = argv
7
+ end
8
+
9
+ def run
10
+ # TODO: allow destructuring: command, *files = @argv
11
+ command = @argv.first
12
+ files = @argv[1..-1]
13
+ err_msg = catch :error do
14
+ case command
15
+ when "watch"
16
+ files = files.map do |f|
17
+ path = Pathname.new(f).absolute? ? f : File.join(Dir.pwd, f)
18
+ if Pathname.new(path).exist?
19
+ path
20
+ else
21
+ throw :error, "Pathname #{f} does not exist"
22
+ end
23
+ end
24
+ Watcher.new(files).watch
25
+ when "build"
26
+ (files.empty? ? Dir.glob("#{Config.src_dir}/**/*.gl") : files).each do |fp|
27
+ Gloss.logger.info "Building #{fp}"
28
+ content = File.read(fp)
29
+ tree_hash = Parser.new(content).run
30
+ type_checker = TypeChecker.new
31
+ rb_output = Builder.new(tree_hash, type_checker).run
32
+ type_checker.run(rb_output)
33
+
34
+ Gloss.logger.info "Writing #{fp}"
35
+ Writer.new(rb_output, fp).run
36
+ end
37
+ when "init"
38
+ force = false
39
+ OptionParser.new do |opt|
40
+ opt.on("--force", "-f") { force = true }
41
+ end.parse(@argv)
42
+ Initializer.new(force).run
43
+ else
44
+ throw :error, "Gloss doesn't know how to #{command}"
45
+ end
46
+ nil
47
+ end
48
+
49
+ if err_msg
50
+ Gloss.logger.fatal err_msg
51
+ exit 1
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ostruct"
4
+ require "yaml"
5
+
6
+ module Gloss
7
+ CONFIG_PATH = ".gloss.yml"
8
+ Config = OpenStruct.new(
9
+ default_config: {
10
+ frozen_string_literals: true,
11
+ src_dir: "src",
12
+ }
13
+ )
14
+
15
+ user_config = if File.exist?(CONFIG_PATH)
16
+ YAML.safe_load(File.read(CONFIG_PATH))
17
+ else
18
+ Config.default_config
19
+ end
20
+ Config.default_config.each { |k, v| Config.send(:"#{k}=", user_config[k.to_s] || v) }
21
+ end
@@ -0,0 +1,11 @@
1
+ module Gloss
2
+ module Errors
3
+ abstract class BaseGlossError < StandardError; end
4
+
5
+ class TypeValidationError < BaseGlossError; end
6
+
7
+ class TypeError < BaseGlossError; end
8
+
9
+ class ParserError < BaseGlossError; end
10
+ end
11
+ end
@@ -0,0 +1,20 @@
1
+ require "yaml"
2
+
3
+ module Gloss
4
+ class Initializer
5
+ def initialize(@force)
6
+ end
7
+
8
+ def run
9
+ if File.exist?(CONFIG_PATH) && !@force
10
+ throw :error, "#{CONFIG_PATH} file already exists - aborting. Use --force to override."
11
+ end
12
+
13
+ File.open(CONFIG_PATH, "wb") do |file|
14
+ file.puts Config.default_config.transform_keys(&:to_s).to_yaml
15
+ end
16
+
17
+ Gloss.logger.info "Created #{CONFIG_PATH} with default preferences"
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,26 @@
1
+ module Gloss
2
+ def self.logger
3
+ if @logger
4
+ @logger
5
+ else
6
+ env_log_level = ENV.fetch("LOG_LEVEL") { "INFO" }
7
+ real_log_level = {
8
+ "UNKNOWN" => Logger::UNKNOWN,
9
+ "FATAL" => Logger::FATAL,
10
+ "ERROR" => Logger::ERROR,
11
+ "WARN" => Logger::WARN,
12
+ "INFO" => Logger::INFO,
13
+ "DEBUG" => Logger::DEBUG,
14
+ "NIL" => nil,
15
+ nil => nil,
16
+ "" => nil
17
+ }.fetch env_log_level
18
+ @logger = Logger.new(real_log_level ? STDOUT : nil)
19
+ formatter = Logger::Formatter.new
20
+ @logger.formatter = proc do |severity, datetime, progname, msg|
21
+ formatter.call(severity, datetime, progname, msg)
22
+ end
23
+ @logger
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,31 @@
1
+ module Gloss
2
+ class Parser
3
+ def initialize(@str : String)
4
+ end
5
+
6
+ def run : String
7
+ tree_json = Gloss.parse_buffer(@str)
8
+ begin
9
+ JSON.parse tree_json, symbolize_names: true
10
+ rescue JSON::ParserError
11
+ error_message = tree_json
12
+ error_message.match /.+\s:(\d+)$/
13
+ if $1
14
+ line_number = $1.to_i
15
+ # line numbers start at 1, but array index starts at 0; so this still gives one line
16
+ # either side of the offending line
17
+ context = @str.lines[(line_number - 2)..(line_number)].map.with_index { |line, index|
18
+ "#{index - 1 + line_number}| #{line}"
19
+ }.join
20
+ error_message = <<~MSG
21
+ #{context.rstrip}
22
+
23
+ #{error_message}
24
+
25
+ MSG
26
+ end
27
+ throw :error, error_message
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Gloss
4
+ class Scope < Hash[String, String]
5
+ def [](k)
6
+ fetch(k) { raise "Undefined expression for current scope: #{k}" }
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Gloss
4
+ class Source < String
5
+ def initialize(@indent_level)
6
+ super()
7
+ end
8
+
9
+ def write(*args : String)
10
+ args.each do |a|
11
+ self << a
12
+ end
13
+ self
14
+ end
15
+
16
+ def write_indnt(*args : String)
17
+ write(*args.map { |a| "#{(" " * @indent_level)}#{a}" })
18
+ end
19
+
20
+ def write_ln(*args : String)
21
+ write_indnt(*args.map { |a| a.strip << "\n" })
22
+ end
23
+
24
+ def increment_indent
25
+ @indent_level += 1
26
+ end
27
+
28
+ def decrement_indent
29
+ @indent_level -= 1
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+ require "pry-byebug"
3
+
4
+ module Gloss
5
+ class TypeChecker
6
+ Project = Struct.new :targets
7
+
8
+ attr_reader :steep_target, :top_level_decls
9
+
10
+ def initialize
11
+ @steep_target = Steep::Project::Target.new(
12
+ 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"],
17
+ ignore_patterns: Array.new,
18
+ signature_patterns: ["sig"]
19
+ )
20
+ @top_level_decls = {}
21
+ end
22
+
23
+ def run(rb_str)
24
+ begin
25
+ valid_types = check_types rb_str
26
+ rescue ParseError => e
27
+ throw :error, ""
28
+ rescue => e
29
+ throw :error, "Type checking Error: #{e.message} (#{e.class})"
30
+ end
31
+
32
+ unless valid_types
33
+ errors = @steep_target.errors.map { |e|
34
+ case e
35
+ when Steep::Diagnostic::Ruby::NoMethod
36
+ "Unknown method :#{e.method}, location: #{e.type.location.inspect}"
37
+ when Steep::Diagnostic::Ruby::MethodBodyTypeMismatch
38
+ "Invalid method body type - expected: #{e.expected}, actual: #{e.actual}"
39
+ when Steep::Diagnostic::Ruby::IncompatibleArguments
40
+ <<-ERR
41
+ Invalid argmuents - method type: #{e.method_type}
42
+ method name: #{e.method_type.method_decls.first.method_name}
43
+ ERR
44
+ when Steep::Diagnostic::Ruby::ReturnTypeMismatch
45
+ "Invalid return type - expected: #{e.expected}, actual: #{e.actual}"
46
+ when Steep::Diagnostic::Ruby::IncompatibleAssignment
47
+ "Invalid assignment - cannot assign #{e.rhs_type} to type #{e.lhs_type}"
48
+ when Steep::Diagnostic::Ruby::UnexpectedBlockGiven
49
+ "Unexpected block given"
50
+ else
51
+ e.inspect
52
+ end
53
+ }.join("\n")
54
+ throw :error, errors
55
+ end
56
+
57
+ true
58
+ end
59
+
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
67
+
68
+ @steep_target.add_source("gloss.rb", rb_str)
69
+
70
+ @top_level_decls.each do |_, decl|
71
+ env << decl
72
+ end
73
+ env = env.resolve_type_names
74
+
75
+ @steep_target.instance_variable_set("@environment", env)
76
+
77
+ @steep_target.type_check
78
+
79
+ if @steep_target.status.is_a? Steep::Project::Target::SignatureErrorStatus
80
+ throw :error, @steep_target.status.errors.map { |e|
81
+ <<~MSG
82
+ SignatureSyntaxError:
83
+ Location: #{e.location}
84
+ Message: "#{e.exception.error_value.value}"
85
+ MSG
86
+ }.join("\n")
87
+ end
88
+
89
+ @steep_target.source_files.each do |path, f|
90
+ if f.status.is_a? Steep::Project::SourceFile::ParseErrorStatus
91
+ e = f.status.error
92
+ throw :error, "#{e.class}: #{e.message}"
93
+ end
94
+ end
95
+
96
+ @steep_target.status.is_a?(Steep::Project::Target::TypeCheckStatus) &&
97
+ @steep_target.no_error? &&
98
+ @steep_target.errors.empty?
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,3 @@
1
+ module Gloss
2
+ VERSION = "0.0.6"
3
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "listen"
4
+
5
+ module Gloss
6
+ class Watcher
7
+ def initialize(@paths : Array[String])
8
+ if @paths.empty?
9
+ @paths = [File.join(Dir.pwd, Config.src_dir)]
10
+ @only = /\.gl$/
11
+ else
12
+ file_names = Array.new
13
+ paths = Array.new
14
+ @paths.each do |pa|
15
+ pn = Pathname.new(pa)
16
+ paths << pn.parent.to_s
17
+ file_names << (pn.file? ? pn.basename.to_s : pa)
18
+ end
19
+ @paths = paths.uniq
20
+ @only = /#{Regexp.union(file_names)}/
21
+ end
22
+ end
23
+
24
+ def watch
25
+ Gloss.logger.info "Now listening for changes in #{@paths.join(', ')}"
26
+ listener = Listen.to(
27
+ *@paths,
28
+ latency: 2,
29
+ only: @only
30
+ ) do |modified, added, removed|
31
+ (modified + added).each do |f|
32
+ Gloss.logger.info "Rewriting #{f}"
33
+ content = File.read(f)
34
+ err = catch :error do
35
+ Writer.new(
36
+ Builder.new(
37
+ Parser.new(
38
+ content
39
+ ).run
40
+ ).run, f
41
+ ).run
42
+ nil
43
+ end
44
+ if err
45
+ Gloss.logger.error err
46
+ else
47
+ Gloss.logger.info "Done"
48
+ end
49
+ end
50
+ removed.each do |f|
51
+ out_path = Utils.src_path_to_output_path(f)
52
+ Gloss.logger.info "Removing #{out_path}"
53
+ File.delete out_path if File.exist? out_path
54
+
55
+ Gloss.logger.info "Done"
56
+ end
57
+ end
58
+ begin
59
+ listener.start
60
+ sleep
61
+ rescue Interrupt
62
+ Gloss.logger.info "Interrupt signal received, shutting down"
63
+ exit 0
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pathname"
4
+ require "fileutils"
5
+
6
+ module Gloss
7
+ module Utils
8
+ module_function
9
+
10
+ def src_path_to_output_path(src_path : String) : String
11
+ src_path.sub("#{Config.src_dir}/", "")
12
+ .sub(/\.gl$/, ".rb")
13
+ end
14
+ end
15
+
16
+ class Writer
17
+ include Utils
18
+
19
+ def initialize(
20
+ @content,
21
+ src_path : String,
22
+ @output_path : Pathname? = Pathname.new(src_path_to_output_path(src_path))
23
+ )
24
+ end
25
+
26
+ def run
27
+ FileUtils.mkdir_p(@output_path.parent) unless @output_path.parent.exist?
28
+ File.open(@output_path, "wb") do |file|
29
+ file.puts @content
30
+ end
31
+ end
32
+ end
33
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: gloss
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.0.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - johansenja
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2020-12-30 00:00:00.000000000 Z
11
+ date: 2021-02-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: fast_blank
@@ -38,6 +38,20 @@ dependencies:
38
38
  - - ">="
39
39
  - !ruby/object:Gem::Version
40
40
  version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rbs
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
41
55
  - !ruby/object:Gem::Dependency
42
56
  name: steep
43
57
  requirement: !ruby/object:Gem::Requirement
@@ -117,40 +131,62 @@ extensions:
117
131
  - ext/gloss/extconf.rb
118
132
  extra_rdoc_files: []
119
133
  files:
134
+ - ".gitattributes"
135
+ - ".github/workflows/crystal_specs.yml"
136
+ - ".github/workflows/ruby_specs.yml"
120
137
  - ".gitignore"
121
138
  - ".gloss.yml"
139
+ - ".rspec"
122
140
  - ".rubocop.yml"
123
141
  - Gemfile
124
142
  - Gemfile.lock
143
+ - LICENSE
125
144
  - README.md
126
145
  - Rakefile
127
146
  - bin/console
128
147
  - exe/gloss
129
148
  - ext/gloss/Makefile
130
149
  - ext/gloss/extconf.rb
150
+ - ext/gloss/lib/cr_ruby.cr
151
+ - ext/gloss/lib/rbs_types.cr
131
152
  - ext/gloss/shard.yml
153
+ - ext/gloss/spec/parser_spec.cr
154
+ - ext/gloss/spec/spec_helper.cr
132
155
  - ext/gloss/src/cr_ast.cr
133
156
  - ext/gloss/src/gloss.cr
134
157
  - ext/gloss/src/lexer.cr
135
- - ext/gloss/src/lib/cr_ruby.cr
136
158
  - ext/gloss/src/parser.cr
137
159
  - ext/gloss/src/rb_ast.cr
138
160
  - gloss.gemspec
139
- - lib/gloss.bundle.dwarf
140
161
  - lib/gloss.rb
141
162
  - lib/gloss/builder.rb
142
163
  - lib/gloss/cli.rb
143
164
  - lib/gloss/config.rb
144
165
  - lib/gloss/errors.rb
145
166
  - lib/gloss/initializer.rb
167
+ - lib/gloss/logger.rb
168
+ - lib/gloss/parser.rb
146
169
  - lib/gloss/scope.rb
147
170
  - lib/gloss/source.rb
171
+ - lib/gloss/type_checker.rb
148
172
  - lib/gloss/version.rb
149
173
  - lib/gloss/watcher.rb
150
174
  - lib/gloss/writer.rb
175
+ - sig/gloss.rbs
151
176
  - sig/listen.rbs
152
- - src/lib/hrb/initializer.gl
153
- - src/lib/hrb/watcher.gl
177
+ - src/lib/gloss/builder.gl
178
+ - src/lib/gloss/cli.gl
179
+ - src/lib/gloss/config.gl
180
+ - src/lib/gloss/errors.gl
181
+ - src/lib/gloss/initializer.gl
182
+ - src/lib/gloss/logger.gl
183
+ - src/lib/gloss/parser.gl
184
+ - src/lib/gloss/scope.gl
185
+ - src/lib/gloss/source.gl
186
+ - src/lib/gloss/type_checker.gl
187
+ - src/lib/gloss/version.gl
188
+ - src/lib/gloss/watcher.gl
189
+ - src/lib/gloss/writer.gl
154
190
  homepage:
155
191
  licenses:
156
192
  - MIT