gloss 0.0.3 → 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (59) 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/.gloss.yml +1 -0
  6. data/.rspec +1 -0
  7. data/Gemfile.lock +10 -12
  8. data/README.md +36 -5
  9. data/Rakefile +1 -1
  10. data/exe/gloss +13 -2
  11. data/ext/gloss/Makefile +8 -19
  12. data/ext/gloss/{src/lib → lib}/cr_ruby.cr +0 -0
  13. data/ext/gloss/lib/rbs_types.cr +3 -0
  14. data/ext/gloss/spec/parser_spec.cr +83 -83
  15. data/ext/gloss/src/cr_ast.cr +96 -77
  16. data/ext/gloss/src/gloss.cr +2 -2
  17. data/ext/gloss/src/lexer.cr +59 -1
  18. data/ext/gloss/src/rb_ast.cr +114 -63
  19. data/lib/gloss.rb +15 -7
  20. data/lib/gloss/cli.rb +85 -28
  21. data/lib/gloss/config.rb +13 -7
  22. data/lib/gloss/errors.rb +3 -4
  23. data/lib/gloss/initializer.rb +9 -9
  24. data/lib/gloss/logger.rb +29 -0
  25. data/lib/gloss/parser.rb +19 -5
  26. data/lib/gloss/prog_loader.rb +141 -0
  27. data/lib/gloss/scope.rb +7 -2
  28. data/lib/gloss/source.rb +17 -14
  29. data/lib/gloss/type_checker.rb +86 -33
  30. data/lib/gloss/utils.rb +44 -0
  31. data/lib/gloss/version.rb +1 -2
  32. data/lib/gloss/visitor.rb +667 -0
  33. data/lib/gloss/watcher.rb +51 -16
  34. data/lib/gloss/writer.rb +24 -15
  35. data/sig/core.rbs +2 -0
  36. data/sig/fast_blank.rbs +4 -0
  37. data/sig/{gloss.rbs → gls.rbs} +0 -0
  38. data/sig/listen.rbs +1 -0
  39. data/sig/optparse.rbs +6 -0
  40. data/sig/rubygems.rbs +9 -0
  41. data/sig/yaml.rbs +3 -0
  42. data/src/exe/gloss +19 -0
  43. data/src/lib/gloss.gl +26 -0
  44. data/src/lib/gloss/cli.gl +70 -0
  45. data/src/lib/gloss/config.gl +9 -3
  46. data/src/lib/gloss/initializer.gl +4 -6
  47. data/src/lib/gloss/logger.gl +21 -0
  48. data/src/lib/gloss/parser.gl +17 -5
  49. data/src/lib/gloss/prog_loader.gl +133 -0
  50. data/src/lib/gloss/scope.gl +7 -0
  51. data/src/lib/gloss/source.gl +32 -0
  52. data/src/lib/gloss/type_checker.gl +85 -36
  53. data/src/lib/gloss/utils.gl +38 -0
  54. data/src/lib/gloss/version.gl +1 -1
  55. data/src/lib/gloss/visitor.gl +575 -0
  56. data/src/lib/gloss/watcher.gl +44 -10
  57. data/src/lib/gloss/writer.gl +16 -14
  58. metadata +28 -8
  59. data/lib/gloss/builder.rb +0 -447
data/lib/gloss/watcher.rb CHANGED
@@ -1,41 +1,76 @@
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
  require "listen"
7
7
  module Gloss
8
8
  class Watcher
9
- def initialize()
10
- @paths = ["src/"]
9
+ def initialize(paths)
10
+ @paths = paths
11
+ (if @paths.empty?
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)}/
30
+ end)
11
31
  end
12
32
  def watch()
13
- puts("=====> Now listening for changes in #{@paths.join(", ")}")
14
- 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|
15
36
  modified.+(added)
16
- .each { |f|
37
+ .each() { |f|
38
+ Gloss.logger
39
+ .info("Rewriting #{f}")
17
40
  content = File.read(f)
18
- Writer.new(Builder.new(content)
41
+ err = catch(:"error") { ||
42
+ Writer.new(Visitor.new(Parser.new(content)
43
+ .run)
19
44
  .run, f)
20
45
  .run
46
+ nil }
47
+ (if err
48
+ Gloss.logger
49
+ .error(err)
50
+ else
51
+ Gloss.logger
52
+ .info("Done")
53
+ end)
21
54
  }
22
- removed.each { |f|
55
+ removed.each() { |f|
23
56
  out_path = Utils.src_path_to_output_path(f)
57
+ Gloss.logger
58
+ .info("Removing #{out_path}")
24
59
  (if File.exist?(out_path)
25
60
  File.delete(out_path)
26
61
  end)
62
+ Gloss.logger
63
+ .info("Done")
27
64
  }
28
65
  }
29
- listener.start
30
66
  begin
31
- loop { ||
32
- sleep(10)
33
- }
67
+ listener.start
68
+ sleep
34
69
  rescue Interrupt
35
- puts("=====> Interrupt signal received, shutting down")
70
+ Gloss.logger
71
+ .info("Interrupt signal received, shutting down")
36
72
  exit(0)
37
73
  end
38
74
  end
39
75
  end
40
76
  end
41
-
data/lib/gloss/writer.rb CHANGED
@@ -1,23 +1,15 @@
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
  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(/\A(?:\.\/)?#{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))
19
- )
10
+ def initialize(content, src_path, output_path = Pathname.new(Utils.src_path_to_output_path(src_path)))
20
11
  @content = content
12
+ @src_path = src_path
21
13
  @output_path = output_path
22
14
  end
23
15
  def run()
@@ -26,9 +18,26 @@ module Gloss
26
18
  FileUtils.mkdir_p(@output_path.parent)
27
19
  end
28
20
  File.open(@output_path, "wb") { |file|
29
- file.<<(@content)
21
+ sb = shebang
22
+ (if sb
23
+ file.puts(sb)
24
+ end)
25
+ file.puts(@content)
30
26
  }
31
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
32
42
  end
33
43
  end
34
-
data/sig/core.rbs ADDED
@@ -0,0 +1,2 @@
1
+ class Any < Object
2
+ end
@@ -0,0 +1,4 @@
1
+ class String
2
+ def blank?: () -> bool
3
+ def blank_as?: () -> bool
4
+ end
File without changes
data/sig/listen.rbs CHANGED
@@ -19,6 +19,7 @@ module Listen
19
19
  ?ignore: Regexp | Array[Regexp],
20
20
  ?ignore!: Regexp,
21
21
  ?only: Regexp?,
22
+ ?latency: (Integer | Float)?,
22
23
  ?polling_fallback_message: String?) {
23
24
  (Array[String] modified, Array[String] added, Array[String] removed) -> void
24
25
  } -> Listener
data/sig/optparse.rbs ADDED
@@ -0,0 +1,6 @@
1
+ # TODO: remove once stdlib gets types for this. Note - RBS
2
+ # already provides some types for this from its `sig` directory - so
3
+ # only adding #parse here
4
+ class OptionParser
5
+ def parse: (Array[String] argv) -> untyped
6
+ end
data/sig/rubygems.rbs ADDED
@@ -0,0 +1,9 @@
1
+ module Gem
2
+ class ConsoleUI
3
+ attr_reader outs: StringIO
4
+ end
5
+
6
+ def self.ui: () -> Gem::ConsoleUI
7
+
8
+ def self.suffixes: () -> Array[String]
9
+ end
data/sig/yaml.rbs ADDED
@@ -0,0 +1,3 @@
1
+ class Yaml
2
+ def safe_load: (String) -> untyped
3
+ end
data/src/exe/gloss ADDED
@@ -0,0 +1,19 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "gloss"
5
+
6
+ begin
7
+ Gloss::CLI.new(ARGV).run
8
+ rescue SystemExit
9
+ # raised by `abort` or `exit`; no op
10
+ rescue => e
11
+ abort <<~MSG
12
+ Unexpected error: #{e.class.name}
13
+ Message: #{e.message}
14
+ Trace:
15
+ #{e.backtrace.join("\n")}
16
+
17
+ This is probably a bug and may warrant a bug report at https://github.com/johansenja/gloss/issues
18
+ MSG
19
+ end
data/src/lib/gloss.gl ADDED
@@ -0,0 +1,26 @@
1
+ require "rbs"
2
+ require "json"
3
+ require "steep"
4
+ require "fast_blank"
5
+
6
+ require "gloss/version"
7
+ require "gloss/cli"
8
+ require "gloss/watcher"
9
+ require "gloss/type_checker"
10
+ require "gloss/parser"
11
+ require "gloss/initializer"
12
+ require "gloss/config"
13
+ require "gloss/writer"
14
+ require "gloss/source"
15
+ require "gloss/scope"
16
+ require "gloss/visitor"
17
+ require "gloss/errors"
18
+ require "gloss/logger"
19
+ require "gloss/prog_loader"
20
+ require "gloss/utils"
21
+
22
+ is_ci = ENV.fetch("CI") { false }
23
+ require "gls" unless is_ci # a bit of a hack for now
24
+
25
+ EMPTY_ARRAY = Array.new.freeze
26
+ EMPTY_HASH = {}.freeze
@@ -0,0 +1,70 @@
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 "init"
16
+ force = false
17
+ OptionParser.new do |opt|
18
+ opt.on("--force", "-f") { force = true }
19
+ end.parse(@argv)
20
+ Initializer.new(force).run
21
+ when "version", "--version", "-v"
22
+ puts Gloss::VERSION
23
+ when "watch", "build"
24
+ type_checker = ProgLoader.new.run
25
+ if command == "watch"
26
+ files = files.map do |f|
27
+ path = Pathname.new(f).absolute? ? f : File.join(Dir.pwd, f)
28
+ if Pathname.new(path).exist?
29
+ path
30
+ else
31
+ throw :error, "Pathname #{f} does not exist"
32
+ end
33
+ end
34
+ Watcher.new(files).watch
35
+ elsif command == "build"
36
+ entry_tree = Parser.new(File.read(Config.entrypoint)).run
37
+ Visitor.new(entry_tree, type_checker).run
38
+ files = Dir.glob("#{Config.src_dir}/**/*.gl") if files.empty?
39
+ files.each do |fp|
40
+ fp = File.absolute_path(fp)
41
+ preloaded_output = OUTPUT_BY_PATH.fetch(fp) { nil }
42
+ if preloaded_output
43
+ rb_output = preloaded_output
44
+ else
45
+ Gloss.logger.info "Building #{fp}"
46
+ content = File.read(fp)
47
+ tree_hash = Parser.new(content).run
48
+ rb_output = Visitor.new(tree_hash, type_checker).run
49
+ end
50
+ Gloss.logger.info "Type checking #{fp}"
51
+ type_checker.run(fp, rb_output)
52
+ end
53
+ # ensure all files are type checked before anything is written
54
+ files.each do |fp|
55
+ fp = File.absolute_path(fp)
56
+ rb_output = OUTPUT_BY_PATH.fetch(fp)
57
+ Gloss.logger.info "Writing #{fp}"
58
+ Writer.new(rb_output, fp).run
59
+ end
60
+ end
61
+ else
62
+ throw :error, "Gloss doesn't know how to #{command}"
63
+ end
64
+ nil
65
+ end
66
+
67
+ abort err_msg if err_msg
68
+ end
69
+ end
70
+ end
@@ -1,15 +1,21 @@
1
- # frozen_string_literal: true
2
-
3
1
  require "ostruct"
4
2
  require "yaml"
5
3
 
6
4
  module Gloss
7
- user_config = YAML.safe_load(File.read(".gloss.yml"))
5
+ CONFIG_PATH = ".gloss.yml"
8
6
  Config = OpenStruct.new(
9
7
  default_config: {
10
8
  frozen_string_literals: true,
11
9
  src_dir: "src",
10
+ entrypoint: nil,
11
+ strict_require: false
12
12
  }
13
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
14
20
  Config.default_config.each { |k, v| Config.send(:"#{k}=", user_config[k.to_s] || v) }
15
21
  end
@@ -1,5 +1,3 @@
1
- # frozen_string_literal: true
2
-
3
1
  require "yaml"
4
2
 
5
3
  module Gloss
@@ -8,15 +6,15 @@ module Gloss
8
6
  end
9
7
 
10
8
  def run
11
- if File.exist?(".gloss.yml") && !@force
12
- abort ".gloss.yml file already exists - aborting. Use --force to override."
9
+ if File.exist?(CONFIG_PATH) && !@force
10
+ throw :error, "#{CONFIG_PATH} file already exists - aborting. Use --force to override."
13
11
  end
14
12
 
15
- File.open(".gloss.yml", "wb") do |file|
13
+ File.open(CONFIG_PATH, "wb") do |file|
16
14
  file.puts Config.default_config.transform_keys(&:to_s).to_yaml
17
15
  end
18
16
 
19
- puts "Created .gloss.yml with default preferences"
17
+ Gloss.logger.info "Created #{CONFIG_PATH} with default preferences"
20
18
  end
21
19
  end
22
20
  end
@@ -0,0 +1,21 @@
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 : IO::NULL)
19
+ end
20
+ end
21
+ end
@@ -1,5 +1,3 @@
1
- # frozen_string_literal: true
2
-
3
1
  module Gloss
4
2
  class Parser
5
3
  def initialize(@str : String)
@@ -10,9 +8,23 @@ module Gloss
10
8
  begin
11
9
  JSON.parse tree_json, symbolize_names: true
12
10
  rescue JSON::ParserError
13
- # if parsing fails then tree is invalid and most likely an error message from the parser in
14
- # crystal
15
- raise Errors::ParserError, tree_json
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
16
28
  end
17
29
  end
18
30
  end
@@ -0,0 +1,133 @@
1
+ require "rubygems/gem_runner"
2
+
3
+ module Gloss
4
+ OUTPUT_BY_PATH = Hash.new
5
+
6
+ class ProgLoader
7
+ def initialize
8
+ entrypoint = Config.entrypoint
9
+ if entrypoint == nil || entrypoint == ""
10
+ throw :error, "Entrypoint is not yet set in .gloss.yml"
11
+ end
12
+ @files_to_process = [
13
+ Utils.absolute_path(Config.entrypoint),
14
+ # __dir__ is typed as String? - but it shouldn't be nil here
15
+ Utils.absolute_path(File.join((__dir__||""), "..", "..", "sig", "core.rbs"))
16
+ ]
17
+ @processed_files = Set.new
18
+ @type_checker = TypeChecker.new
19
+ end
20
+
21
+ def run
22
+ @files_to_process.each do |path_string|
23
+ # currently steep would give an `unexpected jump` if next was used
24
+ unless @processed_files.member?(path_string) || OUTPUT_BY_PATH.[](path_string)
25
+ Gloss.logger.debug "Loading #{path_string}"
26
+ path = Utils.absolute_path(path_string)
27
+ file_contents = File.open(path).read
28
+ contents_tree = Parser.new(file_contents).run
29
+ on_new_file_referenced = proc do |pa, relative|
30
+ if relative
31
+ handle_require_relative pa
32
+ else
33
+ handle_require pa
34
+ end
35
+ end
36
+ OUTPUT_BY_PATH.[](path_string) = Visitor.new(contents_tree, @type_checker, on_new_file_referenced).run
37
+ @processed_files.add path_string
38
+ end
39
+ end
40
+
41
+ @type_checker
42
+ end
43
+
44
+ STDLIB_TYPE_DEPENDENCIES = {
45
+ "yaml" => %w[pstore dbm],
46
+ "rbs" => %w[logger set tsort],
47
+ "logger" => %w[monitor],
48
+ }
49
+
50
+ private def handle_require(path)
51
+ if path.start_with? "."
52
+ base = File.join(Dir.pwd, path)
53
+ fp = base + ".gl"
54
+ if File.exist? fp
55
+ @files_to_process << fp
56
+ end
57
+ return
58
+ end
59
+
60
+ # look for .gl file if the "require" refers to the lib directory of current project dir
61
+ full = File.absolute_path("#{File.join(Config.src_dir, "lib", path)}.gl")
62
+ pathn = Pathname.new full
63
+ if pathn.file?
64
+ @files_to_process << pathn.to_s
65
+ else
66
+ # no .gl file available - .rbs file available?
67
+ # TODO: verify file is still actually requireable
68
+ pathn = Pathname.new("#{File.join(Dir.pwd, "sig", path)}.rbs")
69
+ gem_path = Utils.gem_path_for(path)
70
+ if gem_path
71
+ sig_files = Dir.glob(File.absolute_path(File.join(gem_path, "..", "..", "sig", "**", "*.rbs")))
72
+ if sig_files.length.positive?
73
+ sig_files.each do |fp|
74
+ @type_checker.load_sig_path fp
75
+ end
76
+ @processed_files.add path
77
+ rbs_type_deps = STDLIB_TYPE_DEPENDENCIES.fetch(path) { nil }
78
+ if rbs_type_deps
79
+ rbs_type_deps.each { |d| handle_require d }
80
+ end
81
+ return
82
+ end
83
+ end
84
+
85
+ if pathn.file?
86
+ @type_checker.load_sig_path(pathn.to_s)
87
+ @processed_files.add pathn.to_s
88
+ else
89
+ rbs_stdlib_dir = File.absolute_path(File.join(@type_checker.rbs_gem_dir, "..", "..", "stdlib", path))
90
+ if Pathname.new(rbs_stdlib_dir).exist?
91
+ load_rbs_from_require_path(path)
92
+ rbs_type_deps = STDLIB_TYPE_DEPENDENCIES.fetch(path) { nil }
93
+ if rbs_type_deps
94
+ rbs_type_deps.each { |d| load_rbs_from_require_path d }
95
+ end
96
+ elsif Config.strict_require
97
+ throw :error, "Cannot resolve require path for #{path}"
98
+ else
99
+ Gloss.logger.debug "No path found for #{path}"
100
+ end
101
+ end
102
+ end
103
+ end
104
+
105
+ private def handle_require_relative(path)
106
+ base = File.join(@filepath, "..", path)
107
+ pn : String? = nil
108
+ Gem.suffixes.each do |ext|
109
+ full = File.absolute_path(base + ext)
110
+ pn = full if File.exist?(full)
111
+ end
112
+
113
+ if pn
114
+ @files_to_process << pn unless @files_to_process.include? pn
115
+ elsif Config.strict_require
116
+ throw :error, "Cannot resolve require path for #{pn}"
117
+ else
118
+ Gloss.logger.debug "No path found for #{path}"
119
+ end
120
+ end
121
+
122
+ private def rbs_stdlib_path_for(libr)
123
+ File.absolute_path(File.join(@type_checker.rbs_gem_dir, "..", "..", "stdlib", libr))
124
+ end
125
+
126
+ private def load_rbs_from_require_path(path)
127
+ Dir.glob(File.join(rbs_stdlib_path_for(path), "**", "*.rbs")).each do |fp|
128
+ @type_checker.load_sig_path(fp)
129
+ @processed_files.add fp
130
+ end
131
+ end
132
+ end
133
+ end