gloss 0.0.5 → 0.1.3

Sign up to get free protection for your applications and to get access to all the features.
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