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/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/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,25 @@
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
+ require "gls"
23
+
24
+ EMPTY_ARRAY = Array.new.freeze
25
+ EMPTY_HASH = {}.freeze
data/src/lib/gloss/cli.gl CHANGED
@@ -12,26 +12,52 @@ module Gloss
12
12
  files = @argv[1..-1]
13
13
  err_msg = catch :error do
14
14
  case command
15
- when "watch"
16
- Watcher.new(files).watch
17
- when "build"
18
- (files.empty? ? Dir.glob("#{Config.src_dir}/**/*.gl") : files).each do |fp|
19
- puts "=====> Building #{fp}"
20
- content = File.read(fp)
21
- tree_hash = Parser.new(content).run
22
- type_checker = TypeChecker.new
23
- rb_output = Builder.new(tree_hash, type_checker).run
24
- type_checker.run(rb_output)
25
-
26
- puts "=====> Writing #{fp}"
27
- Writer.new(rb_output, fp).run
28
- end
29
15
  when "init"
30
16
  force = false
31
17
  OptionParser.new do |opt|
32
18
  opt.on("--force", "-f") { force = true }
33
19
  end.parse(@argv)
34
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
35
61
  else
36
62
  throw :error, "Gloss doesn't know how to #{command}"
37
63
  end
@@ -1,5 +1,3 @@
1
- # frozen_string_literal: true
2
-
3
1
  require "ostruct"
4
2
  require "yaml"
5
3
 
@@ -9,6 +7,8 @@ module Gloss
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
14
 
@@ -14,7 +14,7 @@ module Gloss
14
14
  file.puts Config.default_config.transform_keys(&:to_s).to_yaml
15
15
  end
16
16
 
17
- puts "Created #{CONFIG_PATH} with default preferences"
17
+ Gloss.logger.info "Created #{CONFIG_PATH} with default preferences"
18
18
  end
19
19
  end
20
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
@@ -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,10 +1,10 @@
1
- # frozen_string_literal: true
1
+ require "set"
2
2
 
3
3
  module Gloss
4
4
  class TypeChecker
5
5
  Project = Struct.new :targets
6
6
 
7
- attr_reader :steep_target, :top_level_decls
7
+ attr_reader :steep_target, :top_level_decls, :env, :rbs_gem_dir
8
8
 
9
9
  def initialize
10
10
  @steep_target = Steep::Project::Target.new(
@@ -12,62 +12,108 @@ module Gloss
12
12
  options: Steep::Project::Options.new.tap do |o|
13
13
  o.allow_unknown_constant_assignment = true
14
14
  end,
15
- source_patterns: ["gloss.rb"],
15
+ source_patterns: ["**/*.rb"],
16
16
  ignore_patterns: Array.new,
17
17
  signature_patterns: ["sig"]
18
18
  )
19
- @top_level_decls = {}
19
+ @top_level_decls = Set.new
20
+ @rbs_gem_dir = Utils.gem_path_for("rbs")
21
+ env_loader = RBS::EnvironmentLoader.new
22
+ @env = RBS::Environment.from_loader(env_loader)
23
+ project = Steep::Project.new(steepfile_path: Pathname.new(Config.src_dir).realpath)
24
+ project.targets << @steep_target
25
+ loader = Steep::Project::FileLoader.new(project: project)
26
+ #loader.load_signatures
20
27
  end
21
28
 
22
- def run(rb_str)
23
- unless check_types(rb_str)
24
- raise Errors::TypeError,
25
- @steep_target.errors.map { |e|
26
- case e
27
- when Steep::Errors::NoMethod
28
- "Unknown method :#{e.method}, location: #{e.type.location.inspect}"
29
- when Steep::Errors::MethodBodyTypeMismatch
30
- "Invalid method body type - expected: #{e.expected}, actual: #{e.actual}"
31
- when Steep::Errors::IncompatibleArguments
32
- <<-ERR
33
- Invalid argmuents - method type: #{e.method_type}
34
- method name: #{e.method_type.method_decls.first.method_name}
35
- ERR
36
- when Steep::Errors::ReturnTypeMismatch
37
- "Invalid return type - expected: #{e.expected}, actual: #{e.actual}"
38
- when Steep::Errors::IncompatibleAssignment
39
- "Invalid assignment - cannot assign #{e.rhs_type} to type #{e.lhs_type}"
40
- else
41
- e.inspect
42
- end
43
- }.join("\n")
29
+ def run(filepath, rb_str)
30
+ begin
31
+ valid_types = check_types filepath, rb_str
32
+ rescue ParseError => e
33
+ throw :error, ""
34
+ rescue => e
35
+ throw :error, "Type checking Error: #{e.message} (#{e.class})"
36
+ end
37
+
38
+ unless valid_types
39
+ errors = @steep_target.errors.map { |e|
40
+ case e
41
+ when Steep::Diagnostic::Ruby::NoMethod
42
+ "Unknown method :#{e.method}, location: #{e.type.location.inspect}"
43
+ when Steep::Diagnostic::Ruby::MethodBodyTypeMismatch
44
+ "Invalid method body type - expected: #{e.expected}, actual: #{e.actual}"
45
+ when Steep::Diagnostic::Ruby::IncompatibleArguments
46
+ <<-ERR
47
+ Invalid argmuents - method type: #{e.method_types.first}
48
+ method name: #{e.method_name}
49
+ ERR
50
+ when Steep::Diagnostic::Ruby::ReturnTypeMismatch
51
+ "Invalid return type - expected: #{e.expected}, actual: #{e.actual}"
52
+ when Steep::Diagnostic::Ruby::IncompatibleAssignment
53
+ "Invalid assignment - cannot assign #{e.rhs_type} to type #{e.lhs_type}"
54
+ when Steep::Diagnostic::Ruby::UnexpectedBlockGiven
55
+ "Unexpected block given"
56
+ else
57
+ "#{e.header_line}\n#{e.inspect}"
58
+ end
59
+ }.join("\n")
60
+ throw :error, errors
44
61
  end
45
62
 
46
63
  true
47
64
  end
48
65
 
49
- def check_types(rb_str)
50
- env_loader = RBS::EnvironmentLoader.new
51
- env = RBS::Environment.from_loader(env_loader)
52
- project = Steep::Project.new(steepfile_path: Pathname.new(Config.src_dir).realpath)
53
- project.targets << @steep_target
54
- loader = Steep::Project::FileLoader.new(project: project)
55
- loader.load_signatures
66
+ def ready_for_checking!
67
+ @top_level_decls.each do |decl|
68
+ @env << decl
69
+ end
70
+ @env = @env.resolve_type_names
56
71
 
57
- @steep_target.add_source("gloss.rb", rb_str)
72
+ @steep_target.instance_variable_set("@environment", @env)
73
+ end
58
74
 
59
- @top_level_decls.each do |_, decl|
60
- env << decl
61
- end
62
- env = env.resolve_type_names
75
+ def check_types(filepath, rb_str)
76
+ @steep_target.add_source(filepath, rb_str)
63
77
 
64
- @steep_target.instance_variable_set("@environment", env)
78
+ ready_for_checking!
65
79
 
66
80
  @steep_target.type_check
67
81
 
82
+ if @steep_target.status.is_a? Steep::Project::Target::SignatureErrorStatus
83
+ throw :error, @steep_target.status.errors.map { |e|
84
+ msg = case e
85
+ when Steep::Diagnostic::Signature::UnknownTypeName
86
+ "Unknown type name: #{e.name.name} (#{e.location.source[/^.*$/]})"
87
+ when Steep::Diagnostic::Signature::InvalidTypeApplication
88
+ "Invalid type application: #{e.header_line}"
89
+ when Steep::Diagnostic::Signature::DuplicatedMethodDefinition
90
+ "Duplicated method: #{e.header_line}"
91
+ else
92
+ e.header_line
93
+ end
94
+ <<~MSG
95
+ SignatureSyntaxError:
96
+ Location: #{e.location}
97
+ Message: "#{msg}"
98
+ MSG
99
+ }.join("\n")
100
+ end
101
+
102
+ @steep_target.source_files.each do |path, f|
103
+ if f.status.is_a? Steep::Project::SourceFile::ParseErrorStatus
104
+ e = f.status.error
105
+ throw :error, "#{e.class}: #{e.message}"
106
+ end
107
+ end
108
+
68
109
  @steep_target.status.is_a?(Steep::Project::Target::TypeCheckStatus) &&
69
110
  @steep_target.no_error? &&
70
111
  @steep_target.errors.empty?
71
112
  end
113
+
114
+ def load_sig_path(path : String)
115
+ Gloss.logger.debug "Loading signature file for #{path}"
116
+ @steep_target.add_signature path, File.open(path).read
117
+ end
72
118
  end
73
119
  end