gloss 0.0.4 → 0.1.2

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/.gloss.yml +1 -0
  6. data/.rspec +1 -0
  7. data/Gemfile.lock +9 -11
  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/spec/parser_spec.cr +83 -83
  13. data/ext/gloss/src/cr_ast.cr +63 -77
  14. data/ext/gloss/src/gloss.cr +1 -2
  15. data/ext/gloss/src/rb_ast.cr +40 -38
  16. data/lib/gloss.rb +15 -7
  17. data/lib/gloss/cli.rb +75 -32
  18. data/lib/gloss/config.rb +10 -3
  19. data/lib/gloss/errors.rb +1 -1
  20. data/lib/gloss/initializer.rb +6 -5
  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 +5 -5
  29. data/lib/gloss/{builder.rb → visitor.rb} +125 -74
  30. data/lib/gloss/watcher.rb +47 -11
  31. data/lib/gloss/writer.rb +22 -11
  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 +26 -0
  40. data/src/lib/gloss/cli.gl +53 -24
  41. data/src/lib/gloss/config.gl +9 -3
  42. data/src/lib/gloss/initializer.gl +4 -6
  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} +123 -68
  51. data/src/lib/gloss/watcher.gl +44 -10
  52. data/src/lib/gloss/writer.gl +16 -14
  53. metadata +23 -8
data/lib/gloss/config.rb CHANGED
@@ -3,12 +3,19 @@
3
3
  ##### This file was generated by Gloss; any changes made here will be overwritten.
4
4
  ##### See src/ to make changes
5
5
 
6
- require "ostruct"
6
+ require "ostruct"
7
7
  require "yaml"
8
8
  module Gloss
9
- user_config = YAML.safe_load(File.read(".gloss.yml"))
9
+ CONFIG_PATH = ".gloss.yml"
10
10
  Config = OpenStruct.new(default_config: {:frozen_string_literals => true,
11
- :src_dir => "src"}.freeze)
11
+ :src_dir => "src",
12
+ :entrypoint => nil,
13
+ :strict_require => false}.freeze)
14
+ user_config = (if File.exist?(CONFIG_PATH)
15
+ YAML.safe_load(File.read(CONFIG_PATH))
16
+ else
17
+ Config.default_config
18
+ end)
12
19
  Config.default_config
13
20
  .each() { |k, v|
14
21
  Config.send(:"#{k}=", user_config.[](k.to_s) || v)
data/lib/gloss/errors.rb CHANGED
@@ -3,7 +3,7 @@
3
3
  ##### This file was generated by Gloss; any changes made here will be overwritten.
4
4
  ##### See src/ to make changes
5
5
 
6
- module Gloss
6
+ module Gloss
7
7
  module Errors
8
8
  class BaseGlossError < StandardError
9
9
  end
@@ -3,22 +3,23 @@
3
3
  ##### This file was generated by Gloss; any changes made here will be overwritten.
4
4
  ##### See src/ to make changes
5
5
 
6
- require "yaml"
6
+ require "yaml"
7
7
  module Gloss
8
8
  class Initializer
9
9
  def initialize(force)
10
10
  @force = force
11
11
  end
12
12
  def run()
13
- (if File.exist?(".gloss.yml") && !@force
14
- abort(".gloss.yml file already exists - aborting. Use --force to override.")
13
+ (if File.exist?(CONFIG_PATH) && !@force
14
+ throw(:"error", "#{CONFIG_PATH} file already exists - aborting. Use --force to override.")
15
15
  end)
16
- File.open(".gloss.yml", "wb") { |file|
16
+ File.open(CONFIG_PATH, "wb") { |file|
17
17
  file.puts(Config.default_config
18
18
  .transform_keys(&:"to_s")
19
19
  .to_yaml)
20
20
  }
21
- puts("Created .gloss.yml with default preferences")
21
+ Gloss.logger
22
+ .info("Created #{CONFIG_PATH} with default preferences")
22
23
  end
23
24
  end
24
25
  end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ ##### This file was generated by Gloss; any changes made here will be overwritten.
4
+ ##### See src/ to make changes
5
+
6
+ module Gloss
7
+ def self.logger()
8
+ (if @logger
9
+ @logger
10
+ else
11
+ env_log_level = ENV.fetch("LOG_LEVEL") { ||
12
+ "INFO" }
13
+ real_log_level = {"UNKNOWN" => Logger::UNKNOWN,
14
+ "FATAL" => Logger::FATAL,
15
+ "ERROR" => Logger::ERROR,
16
+ "WARN" => Logger::WARN,
17
+ "INFO" => Logger::INFO,
18
+ "DEBUG" => Logger::DEBUG,
19
+ "NIL" => nil,
20
+ nil => nil,
21
+ "" => nil}.fetch(env_log_level)
22
+ @logger = Logger.new((if real_log_level
23
+ STDOUT
24
+ else
25
+ IO::NULL
26
+ end))
27
+ end)
28
+ end
29
+ end
data/lib/gloss/parser.rb CHANGED
@@ -3,7 +3,7 @@
3
3
  ##### This file was generated by Gloss; any changes made here will be overwritten.
4
4
  ##### See src/ to make changes
5
5
 
6
- module Gloss
6
+ module Gloss
7
7
  class Parser
8
8
  def initialize(str)
9
9
  @str = str
@@ -13,7 +13,22 @@
13
13
  begin
14
14
  JSON.parse(tree_json, symbolize_names: true)
15
15
  rescue JSON::ParserError
16
- raise(Errors::ParserError, tree_json)
16
+ error_message = tree_json
17
+ error_message.match(/.+\s:(\d+)$/)
18
+ (if $~.[](1)
19
+ line_number = $~.[](1)
20
+ .to_i
21
+ context = @str.lines
22
+ .[]((( line_number.-(2)
23
+ )..(line_number)))
24
+ .map
25
+ .with_index() { |line, index|
26
+ "#{index.-(1)
27
+ .+(line_number)}| #{line}" }
28
+ .join
29
+ error_message = "#{context.rstrip}\n\n#{error_message}\n"
30
+ end)
31
+ throw(:"error", error_message)
17
32
  end
18
33
  end
19
34
  end
@@ -0,0 +1,141 @@
1
+ # frozen_string_literal: true
2
+
3
+ ##### This file was generated by Gloss; any changes made here will be overwritten.
4
+ ##### See src/ to make changes
5
+
6
+ require "rubygems/gem_runner"
7
+ module Gloss
8
+ OUTPUT_BY_PATH = Hash.new
9
+ class ProgLoader
10
+ def initialize()
11
+ entrypoint = Config.entrypoint
12
+ (if entrypoint.==(nil) || entrypoint.==("")
13
+ throw(:"error", "Entrypoint is not yet set in .gloss.yml")
14
+ end)
15
+ @files_to_process = [Utils.absolute_path(Config.entrypoint), Utils.absolute_path(File.join(__dir__ || "", "..", "..", "sig", "core.rbs"))]
16
+ @processed_files = Set.new
17
+ @type_checker = TypeChecker.new
18
+ end
19
+ def run()
20
+ @files_to_process.each() { |path_string|
21
+ unless @processed_files.member?(path_string) || OUTPUT_BY_PATH.[](path_string)
22
+ Gloss.logger
23
+ .debug("Loading #{path_string}")
24
+ path = Utils.absolute_path(path_string)
25
+ file_contents = File.open(path)
26
+ .read
27
+ contents_tree = Parser.new(file_contents)
28
+ .run
29
+ on_new_file_referenced = proc() { |pa, relative|
30
+ (if relative
31
+ handle_require_relative(pa)
32
+ else
33
+ handle_require(pa)
34
+ end)
35
+ }
36
+ OUTPUT_BY_PATH.[]=(path_string, Visitor.new(contents_tree, @type_checker, on_new_file_referenced)
37
+ .run)
38
+ @processed_files.add(path_string)
39
+ end
40
+ }
41
+ @type_checker
42
+ end
43
+ STDLIB_TYPE_DEPENDENCIES = {"yaml" => ["pstore", "dbm"],
44
+ "rbs" => ["logger", "set", "tsort"],
45
+ "logger" => ["monitor"]}
46
+ private def handle_require(path)
47
+ (if path.start_with?(".")
48
+ base = File.join(Dir.pwd, path)
49
+ fp = base.+(".gl")
50
+ (if File.exist?(fp)
51
+ @files_to_process.<<(fp)
52
+ end)
53
+ return
54
+ end)
55
+ full = File.absolute_path("#{File.join(Config.src_dir, "lib", path)}.gl")
56
+ pathn = Pathname.new(full)
57
+ (if pathn.file?
58
+ @files_to_process.<<(pathn.to_s)
59
+ else
60
+ pathn = Pathname.new("#{File.join(Dir.pwd, "sig", path)}.rbs")
61
+ gem_path = Utils.gem_path_for(path)
62
+ (if gem_path
63
+ sig_files = Dir.glob(File.absolute_path(File.join(gem_path, "..", "..", "sig", "**", "*.rbs")))
64
+ (if sig_files.length
65
+ .positive?
66
+ sig_files.each() { |fp|
67
+ @type_checker.load_sig_path(fp)
68
+ }
69
+ @processed_files.add(path)
70
+ rbs_type_deps = STDLIB_TYPE_DEPENDENCIES.fetch(path) { ||
71
+ nil }
72
+ (if rbs_type_deps
73
+ rbs_type_deps.each() { |d|
74
+ handle_require(d)
75
+ }
76
+ end)
77
+ return
78
+ end)
79
+ end)
80
+ (if pathn.file?
81
+ @type_checker.load_sig_path(pathn.to_s)
82
+ @processed_files.add(pathn.to_s)
83
+ else
84
+ rbs_stdlib_dir = File.absolute_path(File.join(@type_checker.rbs_gem_dir, "..", "..", "stdlib", path))
85
+ (if Pathname.new(rbs_stdlib_dir)
86
+ .exist?
87
+ load_rbs_from_require_path(path)
88
+ rbs_type_deps = STDLIB_TYPE_DEPENDENCIES.fetch(path) { ||
89
+ nil }
90
+ (if rbs_type_deps
91
+ rbs_type_deps.each() { |d|
92
+ load_rbs_from_require_path(d)
93
+ }
94
+ end)
95
+ else
96
+ (if Config.strict_require
97
+ throw(:"error", "Cannot resolve require path for #{path}")
98
+ else
99
+ Gloss.logger
100
+ .debug("No path found for #{path}")
101
+ end)
102
+ end)
103
+ end)
104
+ end)
105
+ end
106
+ private def handle_require_relative(path)
107
+ base = File.join(@filepath, "..", path)
108
+ # @type var pn: String?
109
+ pn = nil
110
+ Gem.suffixes
111
+ .each() { |ext|
112
+ full = File.absolute_path(base.+(ext))
113
+ (if File.exist?(full)
114
+ pn = full
115
+ end)
116
+ }
117
+ (if pn
118
+ unless @files_to_process.include?(pn)
119
+ @files_to_process.<<(pn)
120
+ end
121
+ else
122
+ (if Config.strict_require
123
+ throw(:"error", "Cannot resolve require path for #{pn}")
124
+ else
125
+ Gloss.logger
126
+ .debug("No path found for #{path}")
127
+ end)
128
+ end)
129
+ end
130
+ private def rbs_stdlib_path_for(libr)
131
+ File.absolute_path(File.join(@type_checker.rbs_gem_dir, "..", "..", "stdlib", libr))
132
+ end
133
+ private def load_rbs_from_require_path(path)
134
+ Dir.glob(File.join(rbs_stdlib_path_for(path), "**", "*.rbs"))
135
+ .each() { |fp|
136
+ @type_checker.load_sig_path(fp)
137
+ @processed_files.add(fp)
138
+ }
139
+ end
140
+ end
141
+ end
data/lib/gloss/scope.rb CHANGED
@@ -3,7 +3,7 @@
3
3
  ##### This file was generated by Gloss; any changes made here will be overwritten.
4
4
  ##### See src/ to make changes
5
5
 
6
- module Gloss
6
+ module Gloss
7
7
  class Scope < Hash
8
8
  def [](k)
9
9
  fetch(k) { ||
data/lib/gloss/source.rb CHANGED
@@ -3,7 +3,7 @@
3
3
  ##### This file was generated by Gloss; any changes made here will be overwritten.
4
4
  ##### See src/ to make changes
5
5
 
6
- module Gloss
6
+ module Gloss
7
7
  class Source < String
8
8
  def initialize(indent_level)
9
9
  @indent_level = indent_level
@@ -3,64 +3,112 @@
3
3
  ##### This file was generated by Gloss; any changes made here will be overwritten.
4
4
  ##### See src/ to make changes
5
5
 
6
- module Gloss
6
+ require "set"
7
+ module Gloss
7
8
  class TypeChecker
8
9
  Project = Struct.new(:"targets")
9
- attr_reader(:"steep_target", :"top_level_decls")
10
+ attr_reader(:"steep_target", :"top_level_decls", :"env", :"rbs_gem_dir")
10
11
  def initialize()
11
12
  @steep_target = Steep::Project::Target.new(name: "gloss", options: Steep::Project::Options.new
12
13
  .tap() { |o|
13
14
  o.allow_unknown_constant_assignment=(true)
14
- }, source_patterns: ["gloss.rb"], ignore_patterns: Array.new, signature_patterns: ["sig"])
15
- @top_level_decls = {}
15
+ }, source_patterns: ["**/*.rb"], ignore_patterns: Array.new, signature_patterns: ["sig"])
16
+ @top_level_decls = Set.new
17
+ @rbs_gem_dir = Utils.gem_path_for("rbs")
18
+ env_loader = RBS::EnvironmentLoader.new
19
+ @env = RBS::Environment.from_loader(env_loader)
20
+ project = Steep::Project.new(steepfile_path: Pathname.new(Config.src_dir)
21
+ .realpath)
22
+ project.targets
23
+ .<<(@steep_target)
24
+ loader = Steep::Project::FileLoader.new(project: project)
16
25
  end
17
- def run(rb_str)
18
- unless check_types(rb_str)
19
- raise(Errors::TypeError, @steep_target.errors
26
+ def run(filepath, rb_str)
27
+ begin
28
+ valid_types = check_types(filepath, rb_str)
29
+ rescue ParseError => e
30
+ throw(:"error", "")
31
+ rescue => e
32
+ throw(:"error", "Type checking Error: #{e.message} (#{e.class})")
33
+ end
34
+ unless valid_types
35
+ errors = @steep_target.errors
20
36
  .map() { |e|
21
37
  case e
22
- when Steep::Errors::NoMethod
38
+ when Steep::Diagnostic::Ruby::NoMethod
23
39
  "Unknown method :#{e.method}, location: #{e.type
24
40
  .location
25
41
  .inspect}"
26
- when Steep::Errors::MethodBodyTypeMismatch
42
+ when Steep::Diagnostic::Ruby::MethodBodyTypeMismatch
27
43
  "Invalid method body type - expected: #{e.expected}, actual: #{e.actual}"
28
- when Steep::Errors::IncompatibleArguments
29
- " Invalid argmuents - method type: #{e.method_type}\n method name: #{e.method_type
30
- .method_decls
31
- .first
32
- .method_name}"
33
- when Steep::Errors::ReturnTypeMismatch
44
+ when Steep::Diagnostic::Ruby::IncompatibleArguments
45
+ "Invalid argmuents - method type: #{e.method_types
46
+ .first}\nmethod name: #{e.method_name}"
47
+ when Steep::Diagnostic::Ruby::ReturnTypeMismatch
34
48
  "Invalid return type - expected: #{e.expected}, actual: #{e.actual}"
35
- when Steep::Errors::IncompatibleAssignment
49
+ when Steep::Diagnostic::Ruby::IncompatibleAssignment
36
50
  "Invalid assignment - cannot assign #{e.rhs_type} to type #{e.lhs_type}"
51
+ when Steep::Diagnostic::Ruby::UnexpectedBlockGiven
52
+ "Unexpected block given"
37
53
  else
38
- e.inspect
54
+ "#{e.header_line}\n#{e.inspect}"
39
55
  end
40
56
  }
41
- .join("\n"))
57
+ .join("\n")
58
+ throw(:"error", errors)
42
59
  end
43
60
  true
44
61
  end
45
- def check_types(rb_str)
46
- env_loader = RBS::EnvironmentLoader.new
47
- env = RBS::Environment.from_loader(env_loader)
48
- project = Steep::Project.new(steepfile_path: Pathname.new(Config.src_dir)
49
- .realpath)
50
- project.targets
51
- .<<(@steep_target)
52
- loader = Steep::Project::FileLoader.new(project: project)
53
- loader.load_signatures
54
- @steep_target.add_source("gloss.rb", rb_str)
55
- @top_level_decls.each() { |_, decl|
56
- env.<<(decl)
62
+ def ready_for_checking!()
63
+ @top_level_decls.each() { |decl|
64
+ @env.<<(decl)
57
65
  }
58
- env = env.resolve_type_names
59
- @steep_target.instance_variable_set("@environment", env)
66
+ @env = @env.resolve_type_names
67
+ @steep_target.instance_variable_set("@environment", @env)
68
+ end
69
+ def check_types(filepath, rb_str)
70
+ @steep_target.add_source(filepath, rb_str)
71
+ ready_for_checking!
60
72
  @steep_target.type_check
73
+ (if @steep_target.status
74
+ .is_a?(Steep::Project::Target::SignatureErrorStatus)
75
+ throw(:"error", @steep_target.status
76
+ .errors
77
+ .map() { |e|
78
+ msg = case e
79
+ when Steep::Diagnostic::Signature::UnknownTypeName
80
+ "Unknown type name: #{e.name
81
+ .name} (#{e.location
82
+ .source
83
+ .[](/^.*$/)})"
84
+ when Steep::Diagnostic::Signature::InvalidTypeApplication
85
+ "Invalid type application: #{e.header_line}"
86
+ when Steep::Diagnostic::Signature::DuplicatedMethodDefinition
87
+ "Duplicated method: #{e.header_line}"
88
+ else
89
+ e.header_line
90
+ end
91
+ " SignatureSyntaxError:\n Location: #{e.location}\n Message: \"#{msg}\"" }
92
+ .join("\n"))
93
+ end)
94
+ @steep_target.source_files
95
+ .each() { |path, f|
96
+ (if f.status
97
+ .is_a?(Steep::Project::SourceFile::ParseErrorStatus)
98
+ e = f.status
99
+ .error
100
+ throw(:"error", "#{e.class}: #{e.message}")
101
+ end)
102
+ }
61
103
  @steep_target.status
62
104
  .is_a?(Steep::Project::Target::TypeCheckStatus) && @steep_target.no_error? && @steep_target.errors
63
105
  .empty?
64
106
  end
107
+ def load_sig_path(path)
108
+ Gloss.logger
109
+ .debug("Loading signature file for #{path}")
110
+ @steep_target.add_signature(path, File.open(path)
111
+ .read)
112
+ end
65
113
  end
66
114
  end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ ##### This file was generated by Gloss; any changes made here will be overwritten.
4
+ ##### See src/ to make changes
5
+
6
+ require "rubygems"
7
+ module Gloss
8
+ module Utils
9
+ module_function
10
+ def absolute_path(path)
11
+ pn = Pathname.new(path)
12
+ (if pn.absolute?
13
+ pn.to_s
14
+ else
15
+ ap = File.absolute_path(path)
16
+ (if File.exist?(ap)
17
+ ap
18
+ else
19
+ throw(:"error", "File path #{path} does not exist (also looked for #{ap})")
20
+ end)
21
+ end)
22
+ end
23
+ def gem_path_for(gem_name)
24
+ begin
25
+ Gem.ui
26
+ .instance_variable_set(:"@outs", StringIO.new)
27
+ Gem::GemRunner.new
28
+ .run(["which", gem_name])
29
+ Gem.ui
30
+ .outs
31
+ .string
32
+ rescue SystemExit => e
33
+ nil
34
+ end
35
+ end
36
+ def with_file_header(str)
37
+ "#{Visitor::FILE_HEADER}\n\n#{str}"
38
+ end
39
+ def src_path_to_output_path(src_path)
40
+ src_path.sub("#{Config.src_dir}/", "")
41
+ .sub(/\.gl$/, ".rb")
42
+ end
43
+ end
44
+ end