gloss 0.0.1 → 0.0.6
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.
- checksums.yaml +4 -4
- data/.gitattributes +3 -0
- data/.github/workflows/crystal_specs.yml +26 -0
- data/.github/workflows/ruby_specs.yml +33 -0
- data/.gitignore +2 -1
- data/.rspec +1 -0
- data/Gemfile +0 -1
- data/Gemfile.lock +26 -34
- data/LICENSE +21 -0
- data/README.md +59 -7
- data/Rakefile +4 -0
- data/exe/gloss +15 -1
- data/ext/gloss/Makefile +27 -5
- data/ext/gloss/extconf.rb +3 -25
- data/ext/gloss/{src/lib → lib}/cr_ruby.cr +0 -0
- data/ext/gloss/lib/rbs_types.cr +3 -0
- data/ext/gloss/spec/parser_spec.cr +124 -0
- data/ext/gloss/spec/spec_helper.cr +2 -0
- data/ext/gloss/src/cr_ast.cr +129 -31
- data/ext/gloss/src/gloss.cr +12 -18
- data/ext/gloss/src/lexer.cr +59 -1
- data/ext/gloss/src/parser.cr +548 -254
- data/ext/gloss/src/rb_ast.cr +153 -27
- data/gloss.gemspec +1 -0
- data/lib/gloss.rb +4 -1
- data/lib/gloss/builder.rb +607 -409
- data/lib/gloss/cli.rb +64 -24
- data/lib/gloss/config.rb +16 -10
- data/lib/gloss/errors.rb +13 -7
- data/lib/gloss/initializer.rb +11 -6
- data/lib/gloss/logger.rb +34 -0
- data/lib/gloss/parser.rb +35 -0
- data/lib/gloss/scope.rb +8 -3
- data/lib/gloss/source.rb +18 -15
- data/lib/gloss/type_checker.rb +96 -0
- data/lib/gloss/version.rb +6 -1
- data/lib/gloss/watcher.rb +63 -19
- data/lib/gloss/writer.rb +18 -12
- data/sig/gloss.rbs +3 -0
- data/sig/listen.rbs +1 -0
- data/src/lib/gloss/builder.gl +546 -0
- data/src/lib/gloss/cli.gl +55 -0
- data/src/lib/gloss/config.gl +21 -0
- data/src/lib/gloss/errors.gl +11 -0
- data/src/lib/gloss/initializer.gl +20 -0
- data/src/lib/gloss/logger.gl +26 -0
- data/src/lib/gloss/parser.gl +31 -0
- data/src/lib/gloss/scope.gl +9 -0
- data/src/lib/gloss/source.gl +32 -0
- data/src/lib/gloss/type_checker.gl +101 -0
- data/src/lib/gloss/version.gl +3 -0
- data/src/lib/gloss/watcher.gl +67 -0
- data/src/lib/gloss/writer.gl +33 -0
- metadata +42 -6
- data/lib/gloss.bundle.dwarf +0 -0
- data/src/lib/hrb/initializer.gl +0 -22
- data/src/lib/hrb/watcher.gl +0 -32
data/lib/gloss/cli.rb
CHANGED
@@ -1,35 +1,75 @@
|
|
1
|
-
# frozen_string_literal: true
|
1
|
+
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
3
|
+
##### This file was generated by Gloss; any changes made here will be overwritten.
|
4
|
+
##### See src/ to make changes
|
4
5
|
|
6
|
+
require "optparse"
|
5
7
|
module Gloss
|
6
8
|
class CLI
|
7
9
|
def initialize(argv)
|
8
10
|
@argv = argv
|
9
11
|
end
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
12
|
+
def run()
|
13
|
+
command = @argv.first
|
14
|
+
files = @argv.[](((1)..(-1)))
|
15
|
+
err_msg = catch(:"error") { ||
|
16
|
+
case command
|
17
|
+
when "watch"
|
18
|
+
files = files.map() { |f|
|
19
|
+
path = (if Pathname.new(f)
|
20
|
+
.absolute?
|
21
|
+
f
|
22
|
+
else
|
23
|
+
File.join(Dir.pwd, f)
|
24
|
+
end)
|
25
|
+
(if Pathname.new(path)
|
26
|
+
.exist?
|
27
|
+
path
|
28
|
+
else
|
29
|
+
throw(:"error", "Pathname #{f} does not exist")
|
30
|
+
end)
|
31
|
+
}
|
32
|
+
Watcher.new(files)
|
33
|
+
.watch
|
34
|
+
when "build"
|
35
|
+
(if files.empty?
|
36
|
+
Dir.glob("#{Config.src_dir}/**/*.gl")
|
37
|
+
else
|
38
|
+
files
|
39
|
+
end)
|
40
|
+
.each() { |fp|
|
41
|
+
Gloss.logger
|
42
|
+
.info("Building #{fp}")
|
43
|
+
content = File.read(fp)
|
44
|
+
tree_hash = Parser.new(content)
|
45
|
+
.run
|
46
|
+
type_checker = TypeChecker.new
|
47
|
+
rb_output = Builder.new(tree_hash, type_checker)
|
48
|
+
.run
|
49
|
+
type_checker.run(rb_output)
|
50
|
+
Gloss.logger
|
51
|
+
.info("Writing #{fp}")
|
52
|
+
Writer.new(rb_output, fp)
|
53
|
+
.run
|
54
|
+
}
|
55
|
+
when "init"
|
56
|
+
force = false
|
57
|
+
OptionParser.new() { |opt|
|
58
|
+
opt.on("--force", "-f") { ||
|
59
|
+
force = true
|
60
|
+
}
|
61
|
+
}
|
62
|
+
.parse(@argv)
|
63
|
+
Initializer.new(force)
|
64
|
+
.run
|
65
|
+
else
|
66
|
+
throw(:"error", "Gloss doesn't know how to #{command}")
|
23
67
|
end
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
Initializer.new(force).run
|
30
|
-
else
|
31
|
-
abort "Gloss doesn't know how to #{command}"
|
32
|
-
end
|
68
|
+
nil }
|
69
|
+
(if err_msg
|
70
|
+
Gloss.logger.fatal(err_msg)
|
71
|
+
exit(1)
|
72
|
+
end)
|
33
73
|
end
|
34
74
|
end
|
35
75
|
end
|
data/lib/gloss/config.rb
CHANGED
@@ -1,15 +1,21 @@
|
|
1
|
-
# frozen_string_literal: true
|
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
|
2
5
|
|
3
6
|
require "ostruct"
|
4
7
|
require "yaml"
|
5
|
-
|
6
8
|
module Gloss
|
7
|
-
|
8
|
-
Config = OpenStruct.new(
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
9
|
+
CONFIG_PATH = ".gloss.yml"
|
10
|
+
Config = OpenStruct.new(default_config: {:frozen_string_literals => true,
|
11
|
+
:src_dir => "src"}.freeze)
|
12
|
+
user_config = (if File.exist?(CONFIG_PATH)
|
13
|
+
YAML.safe_load(File.read(CONFIG_PATH))
|
14
|
+
else
|
15
|
+
Config.default_config
|
16
|
+
end)
|
17
|
+
Config.default_config
|
18
|
+
.each() { |k, v|
|
19
|
+
Config.send(:"#{k}=", user_config.[](k.to_s) || v)
|
20
|
+
}
|
15
21
|
end
|
data/lib/gloss/errors.rb
CHANGED
@@ -1,11 +1,17 @@
|
|
1
|
-
|
2
|
-
module Errors
|
3
|
-
class BaseGlossError < StandardError; end
|
4
|
-
|
5
|
-
class TypeValidationError < BaseGlossError; end
|
1
|
+
# frozen_string_literal: true
|
6
2
|
|
7
|
-
|
3
|
+
##### This file was generated by Gloss; any changes made here will be overwritten.
|
4
|
+
##### See src/ to make changes
|
8
5
|
|
9
|
-
|
6
|
+
module Gloss
|
7
|
+
module Errors
|
8
|
+
class BaseGlossError < StandardError
|
9
|
+
end
|
10
|
+
class TypeValidationError < BaseGlossError
|
11
|
+
end
|
12
|
+
class TypeError < BaseGlossError
|
13
|
+
end
|
14
|
+
class ParserError < BaseGlossError
|
15
|
+
end
|
10
16
|
end
|
11
17
|
end
|
data/lib/gloss/initializer.rb
CHANGED
@@ -1,4 +1,8 @@
|
|
1
|
-
# frozen_string_literal: true
|
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
|
+
|
2
6
|
require "yaml"
|
3
7
|
module Gloss
|
4
8
|
class Initializer
|
@@ -6,15 +10,16 @@ module Gloss
|
|
6
10
|
@force = force
|
7
11
|
end
|
8
12
|
def run()
|
9
|
-
(if File.exist?(
|
10
|
-
|
13
|
+
(if File.exist?(CONFIG_PATH) && !@force
|
14
|
+
throw(:"error", "#{CONFIG_PATH} file already exists - aborting. Use --force to override.")
|
11
15
|
end)
|
12
|
-
File.open(
|
16
|
+
File.open(CONFIG_PATH, "wb") { |file|
|
13
17
|
file.puts(Config.default_config
|
14
|
-
.transform_keys(&:to_s)
|
18
|
+
.transform_keys(&:"to_s")
|
15
19
|
.to_yaml)
|
16
20
|
}
|
17
|
-
|
21
|
+
Gloss.logger
|
22
|
+
.info("Created #{CONFIG_PATH} with default preferences")
|
18
23
|
end
|
19
24
|
end
|
20
25
|
end
|
data/lib/gloss/logger.rb
ADDED
@@ -0,0 +1,34 @@
|
|
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
|
+
nil
|
26
|
+
end))
|
27
|
+
formatter = Logger::Formatter.new
|
28
|
+
@logger.formatter=(proc() { |severity, datetime, progname, msg|
|
29
|
+
formatter.call(severity, datetime, progname, msg)
|
30
|
+
})
|
31
|
+
@logger
|
32
|
+
end)
|
33
|
+
end
|
34
|
+
end
|
data/lib/gloss/parser.rb
ADDED
@@ -0,0 +1,35 @@
|
|
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
|
+
class Parser
|
8
|
+
def initialize(str)
|
9
|
+
@str = str
|
10
|
+
end
|
11
|
+
def run()
|
12
|
+
tree_json = Gloss.parse_buffer(@str)
|
13
|
+
begin
|
14
|
+
JSON.parse(tree_json, symbolize_names: true)
|
15
|
+
rescue JSON::ParserError
|
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)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
data/lib/gloss/scope.rb
CHANGED
@@ -1,9 +1,14 @@
|
|
1
|
-
# frozen_string_literal: true
|
1
|
+
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
3
|
+
##### This file was generated by Gloss; any changes made here will be overwritten.
|
4
|
+
##### See src/ to make changes
|
5
|
+
|
6
|
+
module Gloss
|
4
7
|
class Scope < Hash
|
5
8
|
def [](k)
|
6
|
-
fetch(k) {
|
9
|
+
fetch(k) { ||
|
10
|
+
raise("Undefined expression for current scope: #{k}")
|
11
|
+
}
|
7
12
|
end
|
8
13
|
end
|
9
14
|
end
|
data/lib/gloss/source.rb
CHANGED
@@ -1,31 +1,34 @@
|
|
1
|
-
# frozen_string_literal: true
|
1
|
+
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
3
|
+
##### This file was generated by Gloss; any changes made here will be overwritten.
|
4
|
+
##### See src/ to make changes
|
5
|
+
|
6
|
+
module Gloss
|
4
7
|
class Source < String
|
5
8
|
def initialize(indent_level)
|
6
|
-
super()
|
7
9
|
@indent_level = indent_level
|
10
|
+
super()
|
8
11
|
end
|
9
|
-
|
10
12
|
def write(*args)
|
11
|
-
args.each
|
12
|
-
self
|
13
|
-
|
13
|
+
args.each() { |a|
|
14
|
+
self.<<(a)
|
15
|
+
}
|
16
|
+
self
|
14
17
|
end
|
15
|
-
|
16
18
|
def write_indnt(*args)
|
17
|
-
write(*args.map { |a|
|
19
|
+
write(*args.map() { |a|
|
20
|
+
"#{" ".*(@indent_level)}#{a}" })
|
18
21
|
end
|
19
|
-
|
20
22
|
def write_ln(*args)
|
21
|
-
write_indnt(*args.map { |a|
|
23
|
+
write_indnt(*args.map() { |a|
|
24
|
+
a.strip
|
25
|
+
.<<("\n")
|
26
|
+
})
|
22
27
|
end
|
23
|
-
|
24
|
-
def increment_indent
|
28
|
+
def increment_indent()
|
25
29
|
@indent_level += 1
|
26
30
|
end
|
27
|
-
|
28
|
-
def decrement_indent
|
31
|
+
def decrement_indent()
|
29
32
|
@indent_level -= 1
|
30
33
|
end
|
31
34
|
end
|
@@ -0,0 +1,96 @@
|
|
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 "pry-byebug"
|
7
|
+
module Gloss
|
8
|
+
class TypeChecker
|
9
|
+
Project = Struct.new(:"targets")
|
10
|
+
attr_reader(:"steep_target", :"top_level_decls")
|
11
|
+
def initialize()
|
12
|
+
@steep_target = Steep::Project::Target.new(name: "gloss", options: Steep::Project::Options.new
|
13
|
+
.tap() { |o|
|
14
|
+
o.allow_unknown_constant_assignment=(true)
|
15
|
+
}, source_patterns: ["gloss.rb"], ignore_patterns: Array.new, signature_patterns: ["sig"])
|
16
|
+
@top_level_decls = {}
|
17
|
+
end
|
18
|
+
def run(rb_str)
|
19
|
+
begin
|
20
|
+
valid_types = check_types(rb_str)
|
21
|
+
rescue ParseError => e
|
22
|
+
throw(:"error", "")
|
23
|
+
rescue => e
|
24
|
+
throw(:"error", "Type checking Error: #{e.message} (#{e.class})")
|
25
|
+
end
|
26
|
+
unless valid_types
|
27
|
+
errors = @steep_target.errors
|
28
|
+
.map() { |e|
|
29
|
+
case e
|
30
|
+
when Steep::Diagnostic::Ruby::NoMethod
|
31
|
+
"Unknown method :#{e.method}, location: #{e.type
|
32
|
+
.location
|
33
|
+
.inspect}"
|
34
|
+
when Steep::Diagnostic::Ruby::MethodBodyTypeMismatch
|
35
|
+
"Invalid method body type - expected: #{e.expected}, actual: #{e.actual}"
|
36
|
+
when Steep::Diagnostic::Ruby::IncompatibleArguments
|
37
|
+
"Invalid argmuents - method type: #{e.method_type}\nmethod name: #{e.method_type
|
38
|
+
.method_decls
|
39
|
+
.first
|
40
|
+
.method_name}"
|
41
|
+
when Steep::Diagnostic::Ruby::ReturnTypeMismatch
|
42
|
+
"Invalid return type - expected: #{e.expected}, actual: #{e.actual}"
|
43
|
+
when Steep::Diagnostic::Ruby::IncompatibleAssignment
|
44
|
+
"Invalid assignment - cannot assign #{e.rhs_type} to type #{e.lhs_type}"
|
45
|
+
when Steep::Diagnostic::Ruby::UnexpectedBlockGiven
|
46
|
+
"Unexpected block given"
|
47
|
+
else
|
48
|
+
e.inspect
|
49
|
+
end
|
50
|
+
}
|
51
|
+
.join("\n")
|
52
|
+
throw(:"error", errors)
|
53
|
+
end
|
54
|
+
true
|
55
|
+
end
|
56
|
+
def check_types(rb_str)
|
57
|
+
env_loader = RBS::EnvironmentLoader.new
|
58
|
+
env = RBS::Environment.from_loader(env_loader)
|
59
|
+
project = Steep::Project.new(steepfile_path: Pathname.new(Config.src_dir)
|
60
|
+
.realpath)
|
61
|
+
project.targets
|
62
|
+
.<<(@steep_target)
|
63
|
+
loader = Steep::Project::FileLoader.new(project: project)
|
64
|
+
loader.load_signatures
|
65
|
+
@steep_target.add_source("gloss.rb", rb_str)
|
66
|
+
@top_level_decls.each() { |_, decl|
|
67
|
+
env.<<(decl)
|
68
|
+
}
|
69
|
+
env = env.resolve_type_names
|
70
|
+
@steep_target.instance_variable_set("@environment", env)
|
71
|
+
@steep_target.type_check
|
72
|
+
(if @steep_target.status
|
73
|
+
.is_a?(Steep::Project::Target::SignatureErrorStatus)
|
74
|
+
throw(:"error", @steep_target.status
|
75
|
+
.errors
|
76
|
+
.map() { |e|
|
77
|
+
" SignatureSyntaxError:\n Location: #{e.location}\n Message: \"#{e.exception
|
78
|
+
.error_value
|
79
|
+
.value}\"" }
|
80
|
+
.join("\n"))
|
81
|
+
end)
|
82
|
+
@steep_target.source_files
|
83
|
+
.each() { |path, f|
|
84
|
+
(if f.status
|
85
|
+
.is_a?(Steep::Project::SourceFile::ParseErrorStatus)
|
86
|
+
e = f.status
|
87
|
+
.error
|
88
|
+
throw(:"error", "#{e.class}: #{e.message}")
|
89
|
+
end)
|
90
|
+
}
|
91
|
+
@steep_target.status
|
92
|
+
.is_a?(Steep::Project::Target::TypeCheckStatus) && @steep_target.no_error? && @steep_target.errors
|
93
|
+
.empty?
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
data/lib/gloss/version.rb
CHANGED
data/lib/gloss/watcher.rb
CHANGED
@@ -1,31 +1,75 @@
|
|
1
|
-
# frozen_string_literal: true
|
1
|
+
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
3
|
+
##### This file was generated by Gloss; any changes made here will be overwritten.
|
4
|
+
##### See src/ to make changes
|
4
5
|
|
6
|
+
require "listen"
|
5
7
|
module Gloss
|
6
8
|
class Watcher
|
7
|
-
def initialize
|
8
|
-
@paths =
|
9
|
+
def initialize(paths)
|
10
|
+
@paths = paths
|
11
|
+
(if @paths.empty?
|
12
|
+
@paths = [File.join(Dir.pwd, Config.src_dir)]
|
13
|
+
@only = /\.gl$/
|
14
|
+
else
|
15
|
+
file_names = Array.new
|
16
|
+
paths = Array.new
|
17
|
+
@paths.each() { |pa|
|
18
|
+
pn = Pathname.new(pa)
|
19
|
+
paths.<<(pn.parent
|
20
|
+
.to_s)
|
21
|
+
file_names.<<((if pn.file?
|
22
|
+
pn.basename
|
23
|
+
.to_s
|
24
|
+
else
|
25
|
+
pa
|
26
|
+
end))
|
27
|
+
}
|
28
|
+
@paths = paths.uniq
|
29
|
+
@only = /#{Regexp.union(file_names)}/
|
30
|
+
end)
|
9
31
|
end
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
listener = Listen.to(*@paths, latency: 2)
|
14
|
-
(
|
32
|
+
def watch()
|
33
|
+
Gloss.logger
|
34
|
+
.info("Now listening for changes in #{@paths.join(", ")}")
|
35
|
+
listener = Listen.to(*@paths, latency: 2, only: @only) { |modified, added, removed|
|
36
|
+
modified.+(added)
|
37
|
+
.each() { |f|
|
38
|
+
Gloss.logger
|
39
|
+
.info("Rewriting #{f}")
|
15
40
|
content = File.read(f)
|
16
|
-
|
17
|
-
|
18
|
-
|
41
|
+
err = catch(:"error") { ||
|
42
|
+
Writer.new(Builder.new(Parser.new(content)
|
43
|
+
.run)
|
44
|
+
.run, f)
|
45
|
+
.run
|
46
|
+
nil }
|
47
|
+
(if err
|
48
|
+
Gloss.logger
|
49
|
+
.error(err)
|
50
|
+
else
|
51
|
+
Gloss.logger
|
52
|
+
.info("Done")
|
53
|
+
end)
|
54
|
+
}
|
55
|
+
removed.each() { |f|
|
19
56
|
out_path = Utils.src_path_to_output_path(f)
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
57
|
+
Gloss.logger
|
58
|
+
.info("Removing #{out_path}")
|
59
|
+
(if File.exist?(out_path)
|
60
|
+
File.delete(out_path)
|
61
|
+
end)
|
62
|
+
Gloss.logger
|
63
|
+
.info("Done")
|
64
|
+
}
|
65
|
+
}
|
24
66
|
begin
|
25
|
-
|
67
|
+
listener.start
|
68
|
+
sleep
|
26
69
|
rescue Interrupt
|
27
|
-
|
28
|
-
|
70
|
+
Gloss.logger
|
71
|
+
.info("Interrupt signal received, shutting down")
|
72
|
+
exit(0)
|
29
73
|
end
|
30
74
|
end
|
31
75
|
end
|