parlour 3.0.0 → 5.0.0.beta.3
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/.github/ISSUE_TEMPLATE/bug-report.md +0 -0
- data/.github/ISSUE_TEMPLATE/feature-request.md +0 -0
- data/.gitignore +1 -1
- data/.parlour +5 -0
- data/.rspec +0 -0
- data/.travis.yml +0 -0
- data/CHANGELOG.md +57 -0
- data/CODE_OF_CONDUCT.md +0 -0
- data/Gemfile +0 -0
- data/LICENSE.txt +0 -0
- data/README.md +233 -19
- data/Rakefile +0 -0
- data/exe/parlour +109 -4
- data/lib/parlour.rb +27 -1
- data/lib/parlour/conflict_resolver.rb +31 -9
- data/lib/parlour/conversion/converter.rb +34 -0
- data/lib/parlour/conversion/rbi_to_rbs.rb +223 -0
- data/lib/parlour/debugging.rb +0 -0
- data/lib/parlour/detached_rbi_generator.rb +1 -6
- data/lib/parlour/detached_rbs_generator.rb +25 -0
- data/lib/parlour/generator.rb +34 -0
- data/lib/parlour/kernel_hack.rb +0 -0
- data/lib/parlour/options.rb +71 -0
- data/lib/parlour/parse_error.rb +0 -0
- data/lib/parlour/plugin.rb +1 -1
- data/lib/parlour/rbi_generator.rb +24 -37
- data/lib/parlour/rbi_generator/arbitrary.rb +5 -2
- data/lib/parlour/rbi_generator/attribute.rb +14 -5
- data/lib/parlour/rbi_generator/class_namespace.rb +8 -3
- data/lib/parlour/rbi_generator/constant.rb +28 -8
- data/lib/parlour/rbi_generator/enum_class_namespace.rb +8 -3
- data/lib/parlour/rbi_generator/extend.rb +5 -2
- data/lib/parlour/rbi_generator/include.rb +5 -2
- data/lib/parlour/rbi_generator/method.rb +15 -10
- data/lib/parlour/rbi_generator/module_namespace.rb +7 -2
- data/lib/parlour/rbi_generator/namespace.rb +68 -18
- data/lib/parlour/rbi_generator/parameter.rb +13 -7
- data/lib/parlour/rbi_generator/rbi_object.rb +19 -78
- data/lib/parlour/rbi_generator/struct_class_namespace.rb +9 -2
- data/lib/parlour/rbi_generator/struct_prop.rb +12 -9
- data/lib/parlour/rbi_generator/type_alias.rb +101 -0
- data/lib/parlour/rbs_generator.rb +24 -0
- data/lib/parlour/rbs_generator/arbitrary.rb +92 -0
- data/lib/parlour/rbs_generator/attribute.rb +82 -0
- data/lib/parlour/rbs_generator/block.rb +49 -0
- data/lib/parlour/rbs_generator/class_namespace.rb +106 -0
- data/lib/parlour/rbs_generator/constant.rb +95 -0
- data/lib/parlour/rbs_generator/extend.rb +92 -0
- data/lib/parlour/rbs_generator/include.rb +92 -0
- data/lib/parlour/rbs_generator/interface_namespace.rb +34 -0
- data/lib/parlour/rbs_generator/method.rb +146 -0
- data/lib/parlour/rbs_generator/method_signature.rb +104 -0
- data/lib/parlour/rbs_generator/module_namespace.rb +35 -0
- data/lib/parlour/rbs_generator/namespace.rb +627 -0
- data/lib/parlour/rbs_generator/parameter.rb +146 -0
- data/lib/parlour/rbs_generator/rbs_object.rb +78 -0
- data/lib/parlour/rbs_generator/type_alias.rb +96 -0
- data/lib/parlour/type_loader.rb +25 -12
- data/lib/parlour/type_parser.rb +174 -17
- data/lib/parlour/typed_object.rb +87 -0
- data/lib/parlour/types.rb +539 -0
- data/lib/parlour/version.rb +1 -1
- data/parlour.gemspec +1 -1
- data/plugin_examples/foobar_plugin.rb +0 -0
- data/rbi/parlour.rbi +1856 -0
- metadata +35 -10
- data/lib/parlour/rbi_generator/options.rb +0 -74
@@ -0,0 +1,146 @@
|
|
1
|
+
# typed: true
|
2
|
+
module Parlour
|
3
|
+
class RbsGenerator < Generator
|
4
|
+
# Represents a method parameter with a Sorbet type signature.
|
5
|
+
class Parameter
|
6
|
+
extend T::Sig
|
7
|
+
|
8
|
+
sig do
|
9
|
+
params(
|
10
|
+
name: String,
|
11
|
+
type: T.nilable(Types::TypeLike),
|
12
|
+
required: T::Boolean,
|
13
|
+
).void
|
14
|
+
end
|
15
|
+
# Create a new method parameter.
|
16
|
+
# Note that, in RBS, blocks are not parameters. Use a {Block} instead.
|
17
|
+
#
|
18
|
+
# @example Create a simple Integer parameter named +num+.
|
19
|
+
# Parlour::RbsGenerator::Parameter.new('num', type: 'Integer')
|
20
|
+
# @example Create a nilable array parameter.
|
21
|
+
# Parlour::RbsGenerator::Parameter.new('array_of_strings_or_symbols', type:
|
22
|
+
# Parlour::Types::Nilable.new(
|
23
|
+
# Parlour::Types::Array.new(
|
24
|
+
# Parlour::Types::Union.new('String', 'Symbol')
|
25
|
+
# )
|
26
|
+
# )
|
27
|
+
# )
|
28
|
+
# @example Create an optional parameter.
|
29
|
+
# Parlour::RbsGenerator::Parameter.new('name', type: 'String', default: 'Parlour')
|
30
|
+
#
|
31
|
+
# @param name [String] The name of this parameter. This may start with +*+ or +**+,
|
32
|
+
# ,or end with +:+, which will infer the {kind} of this
|
33
|
+
# parameter. (If it contains none of those, {kind} will be +:normal+.)
|
34
|
+
# @param type [Types::TypeLike, nil] This type of this parameter.
|
35
|
+
# @param required [Boolean] Whether this parameter is required.
|
36
|
+
# @return [void]
|
37
|
+
def initialize(name, type: nil, required: true)
|
38
|
+
name = T.must(name)
|
39
|
+
@name = name
|
40
|
+
|
41
|
+
prefix = /^(\*\*|\*|\&)?/.match(name)&.captures&.first || ''
|
42
|
+
@kind = PREFIXES.rassoc(prefix).first
|
43
|
+
|
44
|
+
@kind = :keyword if kind == :normal && name.end_with?(':')
|
45
|
+
|
46
|
+
@type = type || Parlour::Types::Untyped.new
|
47
|
+
@required = required
|
48
|
+
end
|
49
|
+
|
50
|
+
sig { params(other: Object).returns(T::Boolean) }
|
51
|
+
# Returns true if this instance is equal to another method.
|
52
|
+
#
|
53
|
+
# @param other [Object] The other instance. If this is not a {Parameter} (or a
|
54
|
+
# subclass of it), this will always return false.
|
55
|
+
# @return [Boolean]
|
56
|
+
def ==(other)
|
57
|
+
Parameter === other &&
|
58
|
+
name == other.name &&
|
59
|
+
kind == other.kind &&
|
60
|
+
type == other.type &&
|
61
|
+
required == other.required
|
62
|
+
end
|
63
|
+
|
64
|
+
sig { returns(String) }
|
65
|
+
# The name of this parameter, including any prefixes or suffixes such as
|
66
|
+
# +*+.
|
67
|
+
# @return [String]
|
68
|
+
attr_reader :name
|
69
|
+
|
70
|
+
sig { returns(String) }
|
71
|
+
# The name of this parameter, stripped of any prefixes or suffixes. For
|
72
|
+
# example, +*rest+ would become +rest+, or +foo:+ would become +foo+.
|
73
|
+
#
|
74
|
+
# @return [String]
|
75
|
+
def name_without_kind
|
76
|
+
return T.must(name[0..-2]) if kind == :keyword
|
77
|
+
|
78
|
+
prefix_match = /^(\*\*|\*|\&)?[a-zA-Z_]/.match(name)
|
79
|
+
raise 'unknown prefix' unless prefix_match
|
80
|
+
prefix = prefix_match.captures.first || ''
|
81
|
+
T.must(name[prefix.length..-1])
|
82
|
+
end
|
83
|
+
|
84
|
+
sig { returns(Types::TypeLike) }
|
85
|
+
# This parameter's type.
|
86
|
+
# @return [String]
|
87
|
+
attr_reader :type
|
88
|
+
|
89
|
+
sig { returns(T::Boolean) }
|
90
|
+
# Whether this parameter is required.
|
91
|
+
# @return [Boolean]
|
92
|
+
attr_reader :required
|
93
|
+
|
94
|
+
sig { returns(Symbol) }
|
95
|
+
# The kind of parameter that this is. This will be one of +:normal+,
|
96
|
+
# +:splat+, +:double_splat+, or +:keyword+.
|
97
|
+
# @return [Symbol]
|
98
|
+
attr_reader :kind
|
99
|
+
|
100
|
+
# An array of reserved keywords in RBS which may be used as parameter
|
101
|
+
# names in standard Ruby.
|
102
|
+
# TODO: probably incomplete
|
103
|
+
RBS_KEYWORDS = [
|
104
|
+
'type', 'interface', 'out', 'in', 'instance', 'extension', 'top', 'bot',
|
105
|
+
'self', 'nil', 'void'
|
106
|
+
]
|
107
|
+
|
108
|
+
# A mapping of {kind} values to the characteristic prefixes each kind has.
|
109
|
+
PREFIXES = {
|
110
|
+
normal: '',
|
111
|
+
splat: '*',
|
112
|
+
double_splat: '**',
|
113
|
+
}.freeze
|
114
|
+
|
115
|
+
sig { returns(String) }
|
116
|
+
# A string of how this parameter should be defined in an RBS signature.
|
117
|
+
#
|
118
|
+
# @return [String]
|
119
|
+
def to_rbs_param
|
120
|
+
raise 'blocks are not parameters in RBS' if kind == :block
|
121
|
+
|
122
|
+
t = String === @type ? @type : @type.generate_rbs
|
123
|
+
t = "^#{t}" if Types::Proc === @type
|
124
|
+
|
125
|
+
if RBS_KEYWORDS.include? name_without_kind
|
126
|
+
unless $VERBOSE.nil?
|
127
|
+
print Rainbow("Parlour warning: ").yellow.dark.bold
|
128
|
+
print Rainbow("RBS generation: ").magenta.bright.bold
|
129
|
+
puts "'#{name_without_kind}' is a keyword in RBS, renaming method parameter to '_#{name_without_kind}'"
|
130
|
+
end
|
131
|
+
|
132
|
+
n = "_#{name_without_kind}"
|
133
|
+
else
|
134
|
+
n = name_without_kind
|
135
|
+
end
|
136
|
+
|
137
|
+
# Extra check because "?*something" is invalid
|
138
|
+
((required || (kind != :normal && kind != :keyword)) ? '' : '?') + if kind == :keyword
|
139
|
+
"#{n}: #{t}"
|
140
|
+
else
|
141
|
+
"#{PREFIXES[kind]}#{t} #{n}"
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
# typed: true
|
2
|
+
module Parlour
|
3
|
+
class RbsGenerator < Generator
|
4
|
+
# An abstract class which is subclassed by any classes which can generate
|
5
|
+
# entire lines of an RBS, such as {Namespace} and {Method}. (As an example,
|
6
|
+
# {Parameter} is _not_ a subclass because it does not generate lines, only
|
7
|
+
# segments of definition lines.)
|
8
|
+
# @abstract
|
9
|
+
class RbsObject < TypedObject
|
10
|
+
abstract!
|
11
|
+
|
12
|
+
sig { params(generator: Generator, name: String).void }
|
13
|
+
# Creates a new RBS object.
|
14
|
+
# @note Don't call this directly.
|
15
|
+
#
|
16
|
+
# @param generator [RbsGenerator] The current RbsGenerator.
|
17
|
+
# @param name [String] The name of this module.
|
18
|
+
# @return [void]
|
19
|
+
def initialize(generator, name)
|
20
|
+
super(name)
|
21
|
+
@generator = generator
|
22
|
+
@generated_by = RbsGenerator === generator ? generator.current_plugin : nil
|
23
|
+
end
|
24
|
+
|
25
|
+
sig { returns(Generator) }
|
26
|
+
# The generator which this object belongs to.
|
27
|
+
# @return [Generator]
|
28
|
+
attr_reader :generator
|
29
|
+
|
30
|
+
sig do
|
31
|
+
abstract.params(
|
32
|
+
indent_level: Integer,
|
33
|
+
options: Options
|
34
|
+
).returns(T::Array[String])
|
35
|
+
end
|
36
|
+
# Generates the RBS lines for this object.
|
37
|
+
#
|
38
|
+
# @abstract
|
39
|
+
# @param indent_level [Integer] The indentation level to generate the lines at.
|
40
|
+
# @param options [Options] The formatting options to use.
|
41
|
+
# @return [Array<String>] The RBS lines, formatted as specified.
|
42
|
+
def generate_rbs(indent_level, options); end
|
43
|
+
|
44
|
+
sig do
|
45
|
+
abstract.params(
|
46
|
+
others: T::Array[RbsGenerator::RbsObject]
|
47
|
+
).returns(T::Boolean)
|
48
|
+
end
|
49
|
+
# Given an array of other objects, returns true if they may be merged
|
50
|
+
# into this instance using {merge_into_self}. Each subclass will have its
|
51
|
+
# own criteria on what allows objects to be mergeable.
|
52
|
+
#
|
53
|
+
# @abstract
|
54
|
+
# @param others [Array<RbsGenerator::RbsObject>] An array of other {RbsObject} instances.
|
55
|
+
# @return [Boolean] Whether this instance may be merged with them.
|
56
|
+
def mergeable?(others); end
|
57
|
+
|
58
|
+
sig do
|
59
|
+
abstract.params(
|
60
|
+
others: T::Array[RbsGenerator::RbsObject]
|
61
|
+
).void
|
62
|
+
end
|
63
|
+
# Given an array of other objects, merges them into this one. Each
|
64
|
+
# subclass will do this differently.
|
65
|
+
# You MUST ensure that {mergeable?} is true for those instances.
|
66
|
+
#
|
67
|
+
# @abstract
|
68
|
+
# @param others [Array<RbsGenerator::RbsObject>] An array of other {RbsObject} instances.
|
69
|
+
# @return [void]
|
70
|
+
def merge_into_self(others); end
|
71
|
+
|
72
|
+
sig { overridable.override.returns(String) }
|
73
|
+
def describe
|
74
|
+
'RBS object'
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,96 @@
|
|
1
|
+
# typed: true
|
2
|
+
module Parlour
|
3
|
+
class RbsGenerator < Generator
|
4
|
+
# Represents a type alias.
|
5
|
+
class TypeAlias < RbsObject
|
6
|
+
sig do
|
7
|
+
params(
|
8
|
+
generator: Generator,
|
9
|
+
name: String,
|
10
|
+
type: Types::TypeLike,
|
11
|
+
block: T.nilable(T.proc.params(x: TypeAlias).void)
|
12
|
+
).void
|
13
|
+
end
|
14
|
+
# Creates a new type alias.
|
15
|
+
#
|
16
|
+
# @param name [String] The name of the alias.
|
17
|
+
# @param value [String] The type to alias to.
|
18
|
+
def initialize(generator, name:, type:, &block)
|
19
|
+
super(generator, name)
|
20
|
+
@type = type
|
21
|
+
yield_self(&block) if block
|
22
|
+
end
|
23
|
+
|
24
|
+
# @return [String] The type to alias to.
|
25
|
+
sig { returns(Types::TypeLike) }
|
26
|
+
attr_reader :type
|
27
|
+
|
28
|
+
sig { params(other: Object).returns(T::Boolean) }
|
29
|
+
# Returns true if this instance is equal to another type alias.
|
30
|
+
#
|
31
|
+
# @param other [Object] The other instance. If this is not a {TypeAlias} (or a
|
32
|
+
# subclass of it), this will always return false.
|
33
|
+
# @return [Boolean]
|
34
|
+
def ==(other)
|
35
|
+
TypeAlias === other && name == other.name && type == other.type
|
36
|
+
end
|
37
|
+
|
38
|
+
sig do
|
39
|
+
override.params(
|
40
|
+
indent_level: Integer,
|
41
|
+
options: Options
|
42
|
+
).returns(T::Array[String])
|
43
|
+
end
|
44
|
+
# Generates the RBS lines for this type alias.
|
45
|
+
#
|
46
|
+
# @param indent_level [Integer] The indentation level to generate the lines at.
|
47
|
+
# @param options [Options] The formatting options to use.
|
48
|
+
# @return [Array<String>] The RBS lines, formatted as specified.
|
49
|
+
def generate_rbs(indent_level, options)
|
50
|
+
[options.indented(indent_level,
|
51
|
+
"type #{name} = #{String === @type ? @type : @type.generate_rbs}"
|
52
|
+
)]
|
53
|
+
end
|
54
|
+
|
55
|
+
sig do
|
56
|
+
override.params(
|
57
|
+
others: T::Array[RbsGenerator::RbsObject]
|
58
|
+
).returns(T::Boolean)
|
59
|
+
end
|
60
|
+
# Given an array of {TypeAlias} instances, returns true if they may be
|
61
|
+
# merged into this instance using {merge_into_self}. This is always false.
|
62
|
+
#
|
63
|
+
# @param others [Array<RbiGenerator::RbsObject>] An array of other
|
64
|
+
# {TypeAlias} instances.
|
65
|
+
# @return [Boolean] Whether this instance may be merged with them.
|
66
|
+
def mergeable?(others)
|
67
|
+
others.all? { |other| self == other }
|
68
|
+
end
|
69
|
+
|
70
|
+
sig do
|
71
|
+
override.params(
|
72
|
+
others: T::Array[RbsGenerator::RbsObject]
|
73
|
+
).void
|
74
|
+
end
|
75
|
+
# Given an array of {TypeAlias} instances, merges them into this one.
|
76
|
+
# This particular implementation will simply do nothing, as instances
|
77
|
+
# are only mergeable if they are indentical.
|
78
|
+
# You MUST ensure that {mergeable?} is true for those instances.
|
79
|
+
#
|
80
|
+
# @param others [Array<RbiGenerator::RbsObject>] An array of other
|
81
|
+
# {TypeAlias} instances.
|
82
|
+
# @return [void]
|
83
|
+
def merge_into_self(others)
|
84
|
+
# We don't need to change anything! We only merge identical type alias
|
85
|
+
end
|
86
|
+
|
87
|
+
sig { override.returns(String) }
|
88
|
+
# Returns a human-readable brief string description of this code.
|
89
|
+
#
|
90
|
+
# @return [String]
|
91
|
+
def describe
|
92
|
+
"Type Alias (#{name} = #{type})"
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
data/lib/parlour/type_loader.rb
CHANGED
@@ -10,27 +10,34 @@ module Parlour
|
|
10
10
|
# TODO: make this into a class which stores configuration and passes it to
|
11
11
|
# all typeparsers
|
12
12
|
|
13
|
-
sig { params(source: String, filename: T.nilable(String)).returns(RbiGenerator::Namespace) }
|
13
|
+
sig { params(source: String, filename: T.nilable(String), generator: T.nilable(RbiGenerator)).returns(RbiGenerator::Namespace) }
|
14
14
|
# Converts Ruby source code into a tree of objects.
|
15
15
|
#
|
16
16
|
# @param [String] source The Ruby source code.
|
17
17
|
# @param [String, nil] filename The filename to use when parsing this code.
|
18
18
|
# This may be used in error messages, but is optional.
|
19
19
|
# @return [RbiGenerator::Namespace] The root of the object tree.
|
20
|
-
def self.load_source(source, filename = nil)
|
21
|
-
TypeParser.from_source(filename || '(source)', source).parse_all
|
20
|
+
def self.load_source(source, filename = nil, generator: nil)
|
21
|
+
TypeParser.from_source(filename || '(source)', source, generator: generator).parse_all
|
22
22
|
end
|
23
23
|
|
24
|
-
sig { params(filename: String).returns(RbiGenerator::Namespace) }
|
24
|
+
sig { params(filename: String, generator: T.nilable(RbiGenerator)).returns(RbiGenerator::Namespace) }
|
25
25
|
# Converts Ruby source code into a tree of objects from a file.
|
26
26
|
#
|
27
27
|
# @param [String] filename The name of the file to load code from.
|
28
28
|
# @return [RbiGenerator::Namespace] The root of the object tree.
|
29
|
-
def self.load_file(filename)
|
30
|
-
load_source(File.read(filename), filename)
|
29
|
+
def self.load_file(filename, generator: nil)
|
30
|
+
load_source(File.read(filename), filename, generator: generator)
|
31
|
+
end
|
32
|
+
|
33
|
+
sig do
|
34
|
+
params(
|
35
|
+
root: String,
|
36
|
+
inclusions: T::Array[String],
|
37
|
+
exclusions: T::Array[String],
|
38
|
+
generator: T.nilable(RbiGenerator),
|
39
|
+
).returns(RbiGenerator::Namespace)
|
31
40
|
end
|
32
|
-
|
33
|
-
sig { params(root: String, exclusions: T::Array[String]).returns(RbiGenerator::Namespace) }
|
34
41
|
# Loads an entire Sorbet project using Sorbet's file table, obeying any
|
35
42
|
# "typed: ignore" sigils, into a tree of objects.
|
36
43
|
#
|
@@ -39,12 +46,15 @@ module Parlour
|
|
39
46
|
#
|
40
47
|
# @param [String] root The root of the project; where the "sorbet" directory
|
41
48
|
# and "Gemfile" are located.
|
49
|
+
# @param [Array<String>] inclusions A list of files to include when loading
|
50
|
+
# the project, relative to the given root.
|
42
51
|
# @param [Array<String>] exclusions A list of files to exclude when loading
|
43
52
|
# the project, relative to the given root.
|
44
53
|
# @return [RbiGenerator::Namespace] The root of the object tree.
|
45
|
-
def self.load_project(root, exclusions: [])
|
54
|
+
def self.load_project(root, inclusions: ['.'], exclusions: [], generator: nil)
|
55
|
+
expanded_inclusions = inclusions.map { |i| File.expand_path(i, root) }
|
46
56
|
expanded_exclusions = exclusions.map { |e| File.expand_path(e, root) }
|
47
|
-
|
57
|
+
|
48
58
|
stdin, stdout, stderr, wait_thr = T.unsafe(Open3).popen3(
|
49
59
|
'bundle exec srb tc -p file-table-json',
|
50
60
|
chdir: root
|
@@ -63,14 +73,17 @@ module Parlour
|
|
63
73
|
path = File.expand_path(rel_path, root)
|
64
74
|
|
65
75
|
# Skip this file if it was excluded
|
66
|
-
next if
|
76
|
+
next if !expanded_inclusions.any? { |i| path.start_with?(i) } \
|
77
|
+
|| expanded_exclusions.any? { |e| path.start_with?(e) }
|
67
78
|
|
68
79
|
# There are some entries which are URLs to stdlib
|
69
80
|
next unless File.exist?(path)
|
70
81
|
|
71
|
-
namespaces << load_file(path)
|
82
|
+
namespaces << load_file(path, generator: generator)
|
72
83
|
end
|
73
84
|
|
85
|
+
namespaces.uniq!
|
86
|
+
|
74
87
|
raise 'project is empty' if namespaces.empty?
|
75
88
|
|
76
89
|
first_namespace, *other_namespaces = namespaces
|
data/lib/parlour/type_parser.rb
CHANGED
@@ -83,7 +83,7 @@ module Parlour
|
|
83
83
|
|
84
84
|
extend T::Sig
|
85
85
|
|
86
|
-
sig { params(ast: Parser::AST::Node, unknown_node_errors: T::Boolean).void }
|
86
|
+
sig { params(ast: Parser::AST::Node, unknown_node_errors: T::Boolean, generator: T.nilable(RbiGenerator)).void }
|
87
87
|
# Creates a new {TypeParser} from whitequark/parser AST.
|
88
88
|
#
|
89
89
|
# @param [Parser::AST::Node] The AST.
|
@@ -92,25 +92,26 @@ module Parlour
|
|
92
92
|
# if true, a parse error is raised. Setting this to true is likely to
|
93
93
|
# raise errors for lots of non-RBI Ruby code, but setting it to false
|
94
94
|
# could miss genuine typed objects if Parlour or your code contains a bug.
|
95
|
-
def initialize(ast, unknown_node_errors: false)
|
95
|
+
def initialize(ast, unknown_node_errors: false, generator: nil)
|
96
96
|
@ast = ast
|
97
97
|
@unknown_node_errors = unknown_node_errors
|
98
|
+
@generator = generator || DetachedRbiGenerator.new
|
98
99
|
end
|
99
100
|
|
100
|
-
sig { params(filename: String, source: String).returns(TypeParser) }
|
101
|
+
sig { params(filename: String, source: String, generator: T.nilable(RbiGenerator)).returns(TypeParser) }
|
101
102
|
# Creates a new {TypeParser} from a source file and its filename.
|
102
103
|
#
|
103
104
|
# @param [String] filename A filename. This does not need to be an actual
|
104
105
|
# file; it merely identifies this source.
|
105
106
|
# @param [String] source The Ruby source code.
|
106
107
|
# @return [TypeParser]
|
107
|
-
def self.from_source(filename, source)
|
108
|
+
def self.from_source(filename, source, generator: nil)
|
108
109
|
buffer = Parser::Source::Buffer.new(filename)
|
109
110
|
buffer.source = source
|
110
111
|
|
111
112
|
# || special case handles parser returning nil on an empty file
|
112
113
|
parsed = Parser::CurrentRuby.new.parse(buffer) || Parser::AST::Node.new(:body)
|
113
|
-
TypeParser.new(parsed)
|
114
|
+
TypeParser.new(parsed, generator: generator)
|
114
115
|
end
|
115
116
|
|
116
117
|
sig { returns(Parser::AST::Node) }
|
@@ -122,12 +123,16 @@ module Parlour
|
|
122
123
|
# is encountered.
|
123
124
|
attr_reader :unknown_node_errors
|
124
125
|
|
126
|
+
sig { returns(RbiGenerator) }
|
127
|
+
# @return [RbiGenerator] The {RbiGenerator} to load the source into.
|
128
|
+
attr_accessor :generator
|
129
|
+
|
125
130
|
# Parses the entire source file and returns the resulting root namespace.
|
126
131
|
#
|
127
132
|
# @return [RbiGenerator::Namespace] The root namespace of the parsed source.
|
128
133
|
sig { returns(RbiGenerator::Namespace) }
|
129
134
|
def parse_all
|
130
|
-
root =
|
135
|
+
root = generator.root
|
131
136
|
root.children.concat(parse_path_to_object(NodePath.new([])))
|
132
137
|
root
|
133
138
|
end
|
@@ -162,7 +167,7 @@ module Parlour
|
|
162
167
|
top_level = T.let(nil, T.nilable(RbiGenerator::Namespace))
|
163
168
|
parent_names.each do |n|
|
164
169
|
new_obj = RbiGenerator::Namespace.new(
|
165
|
-
|
170
|
+
generator,
|
166
171
|
n.to_s,
|
167
172
|
false,
|
168
173
|
)
|
@@ -225,7 +230,7 @@ module Parlour
|
|
225
230
|
end
|
226
231
|
|
227
232
|
final_obj = RbiGenerator::StructClassNamespace.new(
|
228
|
-
|
233
|
+
generator,
|
229
234
|
this_name.to_s,
|
230
235
|
final,
|
231
236
|
props,
|
@@ -248,7 +253,7 @@ module Parlour
|
|
248
253
|
end
|
249
254
|
|
250
255
|
final_obj = RbiGenerator::EnumClassNamespace.new(
|
251
|
-
|
256
|
+
generator,
|
252
257
|
this_name.to_s,
|
253
258
|
final,
|
254
259
|
enums,
|
@@ -256,7 +261,7 @@ module Parlour
|
|
256
261
|
)
|
257
262
|
else
|
258
263
|
final_obj = RbiGenerator::ClassNamespace.new(
|
259
|
-
|
264
|
+
generator,
|
260
265
|
this_name.to_s,
|
261
266
|
final,
|
262
267
|
node_to_s(superclass),
|
@@ -288,7 +293,7 @@ module Parlour
|
|
288
293
|
top_level = T.let(nil, T.nilable(RbiGenerator::Namespace))
|
289
294
|
parent_names.each do |n|
|
290
295
|
new_obj = RbiGenerator::Namespace.new(
|
291
|
-
|
296
|
+
generator,
|
292
297
|
n.to_s,
|
293
298
|
false,
|
294
299
|
)
|
@@ -298,7 +303,7 @@ module Parlour
|
|
298
303
|
end if parent_names
|
299
304
|
|
300
305
|
final_obj = RbiGenerator::ModuleNamespace.new(
|
301
|
-
|
306
|
+
generator,
|
302
307
|
this_name.to_s,
|
303
308
|
final,
|
304
309
|
interface,
|
@@ -341,7 +346,7 @@ module Parlour
|
|
341
346
|
when :casgn
|
342
347
|
_, name, body = *node
|
343
348
|
[Parlour::RbiGenerator::Constant.new(
|
344
|
-
|
349
|
+
generator,
|
345
350
|
name: T.must(name).to_s,
|
346
351
|
value: T.must(node_to_s(body)),
|
347
352
|
)]
|
@@ -535,7 +540,7 @@ module Parlour
|
|
535
540
|
# There should only be one ever here, but future-proofing anyway
|
536
541
|
def_names.map do |def_name|
|
537
542
|
RbiGenerator::Method.new(
|
538
|
-
|
543
|
+
generator,
|
539
544
|
def_name,
|
540
545
|
parameters,
|
541
546
|
return_type,
|
@@ -575,7 +580,7 @@ module Parlour
|
|
575
580
|
|
576
581
|
def_names.map do |def_name|
|
577
582
|
RbiGenerator::Attribute.new(
|
578
|
-
|
583
|
+
generator,
|
579
584
|
def_name,
|
580
585
|
attr_direction,
|
581
586
|
attr_type,
|
@@ -662,7 +667,7 @@ module Parlour
|
|
662
667
|
# There should only be one ever here, but future-proofing anyway
|
663
668
|
def_names.map do |def_name|
|
664
669
|
RbiGenerator::Method.new(
|
665
|
-
|
670
|
+
generator,
|
666
671
|
def_name,
|
667
672
|
parameters,
|
668
673
|
return_type,
|
@@ -679,7 +684,7 @@ module Parlour
|
|
679
684
|
|
680
685
|
def_names.map do |def_name|
|
681
686
|
RbiGenerator::Attribute.new(
|
682
|
-
|
687
|
+
generator,
|
683
688
|
def_name,
|
684
689
|
attr_direction,
|
685
690
|
attr_type,
|
@@ -691,8 +696,160 @@ module Parlour
|
|
691
696
|
end
|
692
697
|
end
|
693
698
|
|
699
|
+
sig { params(str: String).returns(Types::Type) }
|
700
|
+
# TODO doc
|
701
|
+
def self.parse_single_type(str)
|
702
|
+
i = TypeParser.from_source('(none)', str)
|
703
|
+
i.parse_node_to_type(i.ast)
|
704
|
+
end
|
705
|
+
|
706
|
+
sig { params(node: Parser::AST::Node).returns(Types::Type) }
|
707
|
+
# Given an AST node representing an RBI type (such as 'T::Array[String]'),
|
708
|
+
# parses it into a generic type.
|
709
|
+
#
|
710
|
+
# @param [Parser::AST::Node] node
|
711
|
+
# @return [Parlour::Types::Type]
|
712
|
+
def parse_node_to_type(node)
|
713
|
+
case node.type
|
714
|
+
when :send
|
715
|
+
target, message, *args = *node
|
716
|
+
|
717
|
+
# Special case: is this a generic type instantiation?
|
718
|
+
if message == :[]
|
719
|
+
names = constant_names(target)
|
720
|
+
known_single_element_collections = [:Array, :Set, :Range, :Enumerator, :Enumerable]
|
721
|
+
|
722
|
+
if names.length == 2 && names[0] == :T &&
|
723
|
+
known_single_element_collections.include?(names[1])
|
724
|
+
|
725
|
+
parse_err "no type in T::#{names[1]}[...]", node if args.nil? || args.empty?
|
726
|
+
parse_err "too many types in T::#{names[1]}[...]", node unless args.length == 1
|
727
|
+
return T.must(Types.const_get(T.must(names[1]))).new(parse_node_to_type(T.must(args.first)))
|
728
|
+
elsif names.length == 2 && names == [:T, :Hash]
|
729
|
+
parse_err "not enough types in T::Hash[...]", node if args.nil? || args.length < 2
|
730
|
+
parse_err "too many types in T::Hash[...]", node unless args.length == 2
|
731
|
+
return Types::Hash.new(
|
732
|
+
parse_node_to_type(args[0]), parse_node_to_type(args[1])
|
733
|
+
)
|
734
|
+
else
|
735
|
+
# TODO
|
736
|
+
warning "user-defined generic types not implemented, treating #{names.last} as untyped", node
|
737
|
+
return Types::Untyped.new
|
738
|
+
end
|
739
|
+
end
|
740
|
+
|
741
|
+
# Special case: is this a proc?
|
742
|
+
# This parsing is pretty simplified, but you'd also have to be doing
|
743
|
+
# something pretty cursed with procs to break this
|
744
|
+
# This checks for (send (send (send (const nil :T) :proc) ...) ...)
|
745
|
+
# That's the right amount of nesting for T.proc.params(...).returns(...)
|
746
|
+
if node.to_a[0].type == :send &&
|
747
|
+
node.to_a[0].to_a[0].type == :send &&
|
748
|
+
node.to_a[0].to_a[0].to_a[1] == :proc &&
|
749
|
+
node.to_a[0].to_a[0].to_a[0].type == :const &&
|
750
|
+
node.to_a[0].to_a[0].to_a[0].to_a == [nil, :T] # yuck
|
751
|
+
|
752
|
+
# Get parameters
|
753
|
+
params_send = node.to_a[0]
|
754
|
+
parse_err "expected 'params' to follow 'T.proc'", node unless params_send.to_a[1] == :params
|
755
|
+
parse_err "expected 'params' to have kwargs", node unless params_send.to_a[2].type == :hash
|
756
|
+
|
757
|
+
parameters = params_send.to_a[2].to_a.map do |pair|
|
758
|
+
name, value = *pair
|
759
|
+
parse_err "expected 'params' name to be symbol", node unless name.type == :sym
|
760
|
+
name = name.to_a[0].to_s
|
761
|
+
value = parse_node_to_type(value)
|
762
|
+
|
763
|
+
RbiGenerator::Parameter.new(name, type: value)
|
764
|
+
end
|
765
|
+
|
766
|
+
# Get return value
|
767
|
+
if node.to_a[1] == :void
|
768
|
+
return_type = nil
|
769
|
+
else
|
770
|
+
_, call, *args = *node
|
771
|
+
parse_err 'expected .returns or .void', node unless call == :returns
|
772
|
+
parse_err 'no argument to .returns', node if args.nil? || args.empty?
|
773
|
+
parse_err 'too many arguments to .returns', node unless args.length == 1
|
774
|
+
return_type = parse_node_to_type(T.must(args.first))
|
775
|
+
end
|
776
|
+
|
777
|
+
return Types::Proc.new(parameters, return_type)
|
778
|
+
end
|
779
|
+
|
780
|
+
# The other options for a valid call are all "T.something" methods
|
781
|
+
parse_err "unexpected call #{node_to_s(node).inspect} in type", node \
|
782
|
+
unless target.type == :const && target.to_a == [nil, :T]
|
783
|
+
|
784
|
+
case message
|
785
|
+
when :nilable
|
786
|
+
parse_err 'no argument to T.nilable', node if args.nil? || args.empty?
|
787
|
+
parse_err 'too many arguments to T.nilable', node unless args.length == 1
|
788
|
+
Types::Nilable.new(parse_node_to_type(T.must(args.first)))
|
789
|
+
when :any
|
790
|
+
Types::Union.new((args || []).map { |x| parse_node_to_type(T.must(x)) })
|
791
|
+
when :all
|
792
|
+
Types::Intersection.new((args || []).map { |x| parse_node_to_type(T.must(x)) })
|
793
|
+
when :let
|
794
|
+
# Not really allowed in a type signature, but handy for generalizing
|
795
|
+
# constant types
|
796
|
+
parse_err 'not enough argument to T.let', node if args.nil? || args.length < 2
|
797
|
+
parse_err 'too many arguments to T.nilable', node unless args.length == 2
|
798
|
+
parse_node_to_type(args[1])
|
799
|
+
when :type_parameter
|
800
|
+
parse_err 'no argument to T.type_parameter', node if args.nil? || args.empty?
|
801
|
+
parse_err 'too many arguments to T.type_parameter', node unless args.length == 1
|
802
|
+
parse_err 'expected T.type_parameter to be passed a symbol', node unless T.must(args.first).type == :sym
|
803
|
+
Types::Raw.new(T.must(args.first.to_a[0].to_s))
|
804
|
+
when :class_of
|
805
|
+
parse_err 'no argument to T.class_of', node if args.nil? || args.empty?
|
806
|
+
parse_err 'too many arguments to T.class_of', node unless args.length == 1
|
807
|
+
Types::Class.new(parse_node_to_type(args[0]))
|
808
|
+
when :untyped
|
809
|
+
parse_err 'T.untyped does not accept arguments', node if !args.nil? && !args.empty?
|
810
|
+
Types::Untyped.new
|
811
|
+
else
|
812
|
+
warning "unknown method T.#{message}, treating as untyped", node
|
813
|
+
Types::Untyped.new
|
814
|
+
end
|
815
|
+
when :const
|
816
|
+
# Special case: T::Boolean
|
817
|
+
if constant_names(node) == [:T, :Boolean]
|
818
|
+
return Types::Boolean.new
|
819
|
+
end
|
820
|
+
|
821
|
+
# Otherwise, just a plain old constant
|
822
|
+
Types::Raw.new(constant_names(node).join('::'))
|
823
|
+
when :array
|
824
|
+
# Tuple
|
825
|
+
Types::Tuple.new(node.to_a.map { |x| parse_node_to_type(T.must(x)) })
|
826
|
+
when :hash
|
827
|
+
# Shape/record
|
828
|
+
keys_to_types = node.to_a.map do |pair|
|
829
|
+
key, value = *pair
|
830
|
+
parse_err "all shape keys must be symbols", node unless key.type == :sym
|
831
|
+
[key.to_a[0], parse_node_to_type(value)]
|
832
|
+
end.to_h
|
833
|
+
|
834
|
+
Types::Record.new(keys_to_types)
|
835
|
+
else
|
836
|
+
parse_err "unable to parse type #{node_to_s(node).inspect}", node
|
837
|
+
end
|
838
|
+
end
|
839
|
+
|
694
840
|
protected
|
695
841
|
|
842
|
+
sig { params(msg: String, node: Parser::AST::Node).void }
|
843
|
+
def warning(msg, node)
|
844
|
+
return if $VERBOSE.nil?
|
845
|
+
|
846
|
+
print Rainbow("Parlour warning: ").yellow.dark.bold
|
847
|
+
print Rainbow("Type generalization: ").magenta.bright.bold
|
848
|
+
puts msg
|
849
|
+
print Rainbow(" └ at code: ").blue.bright.bold
|
850
|
+
puts node_to_s(node)
|
851
|
+
end
|
852
|
+
|
696
853
|
sig { params(node: T.nilable(Parser::AST::Node)).returns(T::Array[Symbol]) }
|
697
854
|
# Given a node representing a simple chain of constants (such as A or
|
698
855
|
# A::B::C), converts that node into an array of the constant names which
|