gloss 0.0.4 → 0.1.2

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/.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