gloss 0.0.6 → 0.1.0

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