gloss 0.0.6 → 0.1.0

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.
data/lib/gloss/watcher.rb CHANGED
@@ -10,7 +10,7 @@ module Gloss
10
10
  @paths = paths
11
11
  (if @paths.empty?
12
12
  @paths = [File.join(Dir.pwd, Config.src_dir)]
13
- @only = /\.gl$/
13
+ @only = /(?:(\.gl|(?:(?<=\/)[^\.\/]+))\z|\A[^\.\/]+\z)/
14
14
  else
15
15
  file_names = Array.new
16
16
  paths = Array.new
@@ -39,7 +39,7 @@ module Gloss
39
39
  .info("Rewriting #{f}")
40
40
  content = File.read(f)
41
41
  err = catch(:"error") { ||
42
- Writer.new(Builder.new(Parser.new(content)
42
+ Writer.new(Visitor.new(Parser.new(content)
43
43
  .run)
44
44
  .run, f)
45
45
  .run
data/lib/gloss/writer.rb CHANGED
@@ -3,20 +3,13 @@
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 "pathname"
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("#{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)))
10
+ def initialize(content, src_path, output_path = Pathname.new(Utils.src_path_to_output_path(src_path)))
19
11
  @content = content
12
+ @src_path = src_path
20
13
  @output_path = output_path
21
14
  end
22
15
  def run()
@@ -25,8 +18,26 @@ module Gloss
25
18
  FileUtils.mkdir_p(@output_path.parent)
26
19
  end
27
20
  File.open(@output_path, "wb") { |file|
21
+ sb = shebang
22
+ (if sb
23
+ file.puts(sb)
24
+ end)
28
25
  file.puts(@content)
29
26
  }
30
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
31
42
  end
32
43
  end
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,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
data/src/lib/gloss/cli.gl CHANGED
@@ -12,44 +12,59 @@ module Gloss
12
12
  files = @argv[1..-1]
13
13
  err_msg = catch :error do
14
14
  case command
15
- when "watch"
16
- files = files.map do |f|
17
- path = Pathname.new(f).absolute? ? f : File.join(Dir.pwd, f)
18
- if Pathname.new(path).exist?
19
- path
20
- else
21
- throw :error, "Pathname #{f} does not exist"
22
- end
23
- end
24
- Watcher.new(files).watch
25
- when "build"
26
- (files.empty? ? Dir.glob("#{Config.src_dir}/**/*.gl") : files).each do |fp|
27
- Gloss.logger.info "Building #{fp}"
28
- content = File.read(fp)
29
- tree_hash = Parser.new(content).run
30
- type_checker = TypeChecker.new
31
- rb_output = Builder.new(tree_hash, type_checker).run
32
- type_checker.run(rb_output)
33
-
34
- Gloss.logger.info "Writing #{fp}"
35
- Writer.new(rb_output, fp).run
36
- end
37
15
  when "init"
38
16
  force = false
39
17
  OptionParser.new do |opt|
40
18
  opt.on("--force", "-f") { force = true }
41
19
  end.parse(@argv)
42
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
43
61
  else
44
62
  throw :error, "Gloss doesn't know how to #{command}"
45
63
  end
46
64
  nil
47
65
  end
48
66
 
49
- if err_msg
50
- Gloss.logger.fatal err_msg
51
- exit 1
52
- end
67
+ abort err_msg if err_msg
53
68
  end
54
69
  end
55
70
  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
 
@@ -15,12 +15,7 @@ module Gloss
15
15
  nil => nil,
16
16
  "" => nil
17
17
  }.fetch env_log_level
18
- @logger = Logger.new(real_log_level ? STDOUT : nil)
19
- formatter = Logger::Formatter.new
20
- @logger.formatter = proc do |severity, datetime, progname, msg|
21
- formatter.call(severity, datetime, progname, msg)
22
- end
23
- @logger
18
+ @logger = Logger.new(real_log_level ? STDOUT : IO::NULL)
24
19
  end
25
20
  end
26
21
  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,11 +1,10 @@
1
- # frozen_string_literal: true
2
- require "pry-byebug"
1
+ require "set"
3
2
 
4
3
  module Gloss
5
4
  class TypeChecker
6
5
  Project = Struct.new :targets
7
6
 
8
- attr_reader :steep_target, :top_level_decls
7
+ attr_reader :steep_target, :top_level_decls, :env, :rbs_gem_dir
9
8
 
10
9
  def initialize
11
10
  @steep_target = Steep::Project::Target.new(
@@ -13,16 +12,23 @@ module Gloss
13
12
  options: Steep::Project::Options.new.tap do |o|
14
13
  o.allow_unknown_constant_assignment = true
15
14
  end,
16
- source_patterns: ["gloss.rb"],
15
+ source_patterns: ["**/*.rb"],
17
16
  ignore_patterns: Array.new,
18
17
  signature_patterns: ["sig"]
19
18
  )
20
- @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
21
27
  end
22
28
 
23
- def run(rb_str)
29
+ def run(filepath, rb_str)
24
30
  begin
25
- valid_types = check_types rb_str
31
+ valid_types = check_types filepath, rb_str
26
32
  rescue ParseError => e
27
33
  throw :error, ""
28
34
  rescue => e
@@ -38,8 +44,8 @@ module Gloss
38
44
  "Invalid method body type - expected: #{e.expected}, actual: #{e.actual}"
39
45
  when Steep::Diagnostic::Ruby::IncompatibleArguments
40
46
  <<-ERR
41
- Invalid argmuents - method type: #{e.method_type}
42
- method name: #{e.method_type.method_decls.first.method_name}
47
+ Invalid argmuents - method type: #{e.method_types.first}
48
+ method name: #{e.method_name}
43
49
  ERR
44
50
  when Steep::Diagnostic::Ruby::ReturnTypeMismatch
45
51
  "Invalid return type - expected: #{e.expected}, actual: #{e.actual}"
@@ -48,7 +54,7 @@ module Gloss
48
54
  when Steep::Diagnostic::Ruby::UnexpectedBlockGiven
49
55
  "Unexpected block given"
50
56
  else
51
- e.inspect
57
+ "#{e.header_line}\n#{e.inspect}"
52
58
  end
53
59
  }.join("\n")
54
60
  throw :error, errors
@@ -57,31 +63,38 @@ module Gloss
57
63
  true
58
64
  end
59
65
 
60
- def check_types(rb_str)
61
- env_loader = RBS::EnvironmentLoader.new
62
- env = RBS::Environment.from_loader(env_loader)
63
- project = Steep::Project.new(steepfile_path: Pathname.new(Config.src_dir).realpath)
64
- project.targets << @steep_target
65
- loader = Steep::Project::FileLoader.new(project: project)
66
- 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
67
71
 
68
- @steep_target.add_source("gloss.rb", rb_str)
72
+ @steep_target.instance_variable_set("@environment", @env)
73
+ end
69
74
 
70
- @top_level_decls.each do |_, decl|
71
- env << decl
72
- end
73
- env = env.resolve_type_names
75
+ def check_types(filepath, rb_str)
76
+ @steep_target.add_source(filepath, rb_str)
74
77
 
75
- @steep_target.instance_variable_set("@environment", env)
78
+ ready_for_checking!
76
79
 
77
80
  @steep_target.type_check
78
81
 
79
82
  if @steep_target.status.is_a? Steep::Project::Target::SignatureErrorStatus
80
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
81
94
  <<~MSG
82
95
  SignatureSyntaxError:
83
96
  Location: #{e.location}
84
- Message: "#{e.exception.error_value.value}"
97
+ Message: "#{msg}"
85
98
  MSG
86
99
  }.join("\n")
87
100
  end
@@ -97,5 +110,10 @@ module Gloss
97
110
  @steep_target.no_error? &&
98
111
  @steep_target.errors.empty?
99
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
100
118
  end
101
119
  end