parlour 1.0.0 → 4.0.1
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 -0
- data/.parlour +5 -0
- data/.rspec +0 -0
- data/.travis.yml +4 -3
- data/CHANGELOG.md +65 -0
- data/CODE_OF_CONDUCT.md +0 -0
- data/Gemfile +0 -0
- data/LICENSE.txt +0 -0
- data/README.md +54 -1
- data/Rakefile +0 -0
- data/exe/parlour +68 -2
- data/lib/parlour.rb +7 -0
- data/lib/parlour/conflict_resolver.rb +129 -19
- data/lib/parlour/debugging.rb +0 -0
- data/lib/parlour/detached_rbi_generator.rb +25 -0
- data/lib/parlour/kernel_hack.rb +2 -0
- data/lib/parlour/parse_error.rb +19 -0
- data/lib/parlour/plugin.rb +1 -1
- data/lib/parlour/rbi_generator.rb +13 -7
- data/lib/parlour/rbi_generator/arbitrary.rb +0 -0
- data/lib/parlour/rbi_generator/attribute.rb +0 -0
- data/lib/parlour/rbi_generator/class_namespace.rb +8 -5
- data/lib/parlour/rbi_generator/constant.rb +11 -2
- data/lib/parlour/rbi_generator/enum_class_namespace.rb +24 -2
- data/lib/parlour/rbi_generator/extend.rb +0 -0
- data/lib/parlour/rbi_generator/include.rb +0 -0
- data/lib/parlour/rbi_generator/method.rb +0 -0
- data/lib/parlour/rbi_generator/module_namespace.rb +6 -4
- data/lib/parlour/rbi_generator/namespace.rb +81 -15
- data/lib/parlour/rbi_generator/options.rb +15 -2
- data/lib/parlour/rbi_generator/parameter.rb +5 -5
- data/lib/parlour/rbi_generator/rbi_object.rb +0 -0
- data/lib/parlour/rbi_generator/struct_class_namespace.rb +103 -0
- data/lib/parlour/rbi_generator/struct_prop.rb +136 -0
- data/lib/parlour/type_loader.rb +104 -0
- data/lib/parlour/type_parser.rb +854 -0
- data/lib/parlour/version.rb +1 -1
- data/parlour.gemspec +6 -5
- data/plugin_examples/foobar_plugin.rb +0 -0
- data/rbi/parlour.rbi +893 -0
- metadata +40 -18
@@ -7,7 +7,7 @@ module Parlour
|
|
7
7
|
|
8
8
|
sig do
|
9
9
|
params(
|
10
|
-
name:
|
10
|
+
name: String,
|
11
11
|
type: T.nilable(String),
|
12
12
|
default: T.nilable(String)
|
13
13
|
).void
|
@@ -42,7 +42,7 @@ module Parlour
|
|
42
42
|
|
43
43
|
@kind = :keyword if kind == :normal && name.end_with?(':')
|
44
44
|
|
45
|
-
@type = type
|
45
|
+
@type = type || 'T.untyped'
|
46
46
|
@default = default
|
47
47
|
end
|
48
48
|
|
@@ -80,10 +80,10 @@ module Parlour
|
|
80
80
|
T.must(name[prefix.length..-1])
|
81
81
|
end
|
82
82
|
|
83
|
-
sig { returns(
|
83
|
+
sig { returns(String) }
|
84
84
|
# A Sorbet string of this parameter's type, such as +"String"+ or
|
85
85
|
# +"T.untyped"+.
|
86
|
-
# @return [String
|
86
|
+
# @return [String]
|
87
87
|
attr_reader :type
|
88
88
|
|
89
89
|
sig { returns(T.nilable(String)) }
|
@@ -118,7 +118,7 @@ module Parlour
|
|
118
118
|
#
|
119
119
|
# @return [String]
|
120
120
|
def to_sig_param
|
121
|
-
"#{name_without_kind}: #{type
|
121
|
+
"#{name_without_kind}: #{type}"
|
122
122
|
end#
|
123
123
|
|
124
124
|
# A mapping of {kind} values to the characteristic prefixes each kind has.
|
File without changes
|
@@ -0,0 +1,103 @@
|
|
1
|
+
# typed: true
|
2
|
+
module Parlour
|
3
|
+
class RbiGenerator
|
4
|
+
# Represents an struct definition; that is, a class which subclasses
|
5
|
+
# +T::Struct+ and declares `prop` members.
|
6
|
+
class StructClassNamespace < ClassNamespace
|
7
|
+
extend T::Sig
|
8
|
+
|
9
|
+
sig do
|
10
|
+
params(
|
11
|
+
generator: RbiGenerator,
|
12
|
+
name: String,
|
13
|
+
final: T::Boolean,
|
14
|
+
props: T::Array[StructProp],
|
15
|
+
abstract: T::Boolean,
|
16
|
+
block: T.nilable(T.proc.params(x: StructClassNamespace).void)
|
17
|
+
).void
|
18
|
+
end
|
19
|
+
# Creates a new struct class definition.
|
20
|
+
# @note You should use {Namespace#create_struct_class} rather than this directly.
|
21
|
+
#
|
22
|
+
# @param generator [RbiGenerator] The current RbiGenerator.
|
23
|
+
# @param name [String] The name of this class.
|
24
|
+
# @param final [Boolean] Whether this namespace is final.
|
25
|
+
# @param props [Array<StructProp>] The props of the struct.
|
26
|
+
# @param abstract [Boolean] A boolean indicating whether this class is abstract.
|
27
|
+
# @param block A block which the new instance yields itself to.
|
28
|
+
# @return [void]
|
29
|
+
def initialize(generator, name, final, props, abstract, &block)
|
30
|
+
super(generator, name, final, 'T::Struct', abstract, &block)
|
31
|
+
@props = props
|
32
|
+
end
|
33
|
+
|
34
|
+
sig { returns(T::Array[StructProp]) }
|
35
|
+
# The props of the struct.
|
36
|
+
# @return [Array<StructProp>]
|
37
|
+
attr_reader :props
|
38
|
+
|
39
|
+
sig do
|
40
|
+
override.params(
|
41
|
+
indent_level: Integer,
|
42
|
+
options: Options
|
43
|
+
).returns(T::Array[String])
|
44
|
+
end
|
45
|
+
# Generates the RBI lines for the body of this struct. This consists of
|
46
|
+
# {props}, {includes}, {extends} and {children}.
|
47
|
+
#
|
48
|
+
# @param indent_level [Integer] The indentation level to generate the lines at.
|
49
|
+
# @param options [Options] The formatting options to use.
|
50
|
+
# @return [Array<String>] The RBI lines for the body, formatted as specified.
|
51
|
+
def generate_body(indent_level, options)
|
52
|
+
result = []
|
53
|
+
props.each do |prop|
|
54
|
+
result << options.indented(indent_level, prop.to_prop_call)
|
55
|
+
end
|
56
|
+
result << ''
|
57
|
+
|
58
|
+
result + super
|
59
|
+
end
|
60
|
+
|
61
|
+
sig do
|
62
|
+
override.params(
|
63
|
+
others: T::Array[RbiGenerator::RbiObject]
|
64
|
+
).returns(T::Boolean)
|
65
|
+
end
|
66
|
+
# Given an array of {StructClassNamespace} instances, returns true if they may
|
67
|
+
# be merged into this instance using {merge_into_self}. For instances to
|
68
|
+
# be mergeable, they must either all be abstract or all not be abstract,
|
69
|
+
# and they must define the same superclass (or none at all).
|
70
|
+
#
|
71
|
+
# @param others [Array<RbiGenerator::RbiObject>] An array of other {StructClassNamespace} instances.
|
72
|
+
# @return [Boolean] Whether this instance may be merged with them.
|
73
|
+
def mergeable?(others)
|
74
|
+
others = T.cast(others, T::Array[Namespace]) rescue (return false)
|
75
|
+
all = others + [self]
|
76
|
+
all_structs = T.cast(all.select { |x| StructClassNamespace === x }, T::Array[StructClassNamespace])
|
77
|
+
|
78
|
+
T.must(super && all_structs.map { |s| s.props.map(&:to_prop_call).sort }.reject(&:empty?).uniq.length <= 1)
|
79
|
+
end
|
80
|
+
|
81
|
+
sig do
|
82
|
+
override.params(
|
83
|
+
others: T::Array[RbiGenerator::RbiObject]
|
84
|
+
).void
|
85
|
+
end
|
86
|
+
# Given an array of {StructClassNamespace} instances, merges them into this one.
|
87
|
+
# You MUST ensure that {mergeable?} is true for those instances.
|
88
|
+
#
|
89
|
+
# @param others [Array<RbiGenerator::RbiObject>] An array of other {StructClassNamespace} instances.
|
90
|
+
# @return [void]
|
91
|
+
def merge_into_self(others)
|
92
|
+
super
|
93
|
+
|
94
|
+
others.each do |other|
|
95
|
+
next unless StructClassNamespace === other
|
96
|
+
other = T.cast(other, StructClassNamespace)
|
97
|
+
|
98
|
+
@props = other.props if props.empty?
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
@@ -0,0 +1,136 @@
|
|
1
|
+
# typed: true
|
2
|
+
module Parlour
|
3
|
+
class RbiGenerator
|
4
|
+
# Represents a +T::Struct+ property.
|
5
|
+
class StructProp
|
6
|
+
extend T::Sig
|
7
|
+
|
8
|
+
sig do
|
9
|
+
params(
|
10
|
+
name: String,
|
11
|
+
type: String,
|
12
|
+
optional: T.nilable(T.any(T::Boolean, Symbol)),
|
13
|
+
enum: T.nilable(String),
|
14
|
+
dont_store: T.nilable(T::Boolean),
|
15
|
+
foreign: T.nilable(String),
|
16
|
+
default: T.nilable(String),
|
17
|
+
factory: T.nilable(String),
|
18
|
+
immutable: T.nilable(T::Boolean),
|
19
|
+
array: T.nilable(String),
|
20
|
+
override: T.nilable(T::Boolean),
|
21
|
+
redaction: T.nilable(String),
|
22
|
+
).void
|
23
|
+
end
|
24
|
+
# Create a new struct property.
|
25
|
+
#
|
26
|
+
# For documentation on all optional properties, please refer to the
|
27
|
+
# documentation for T::Struct within the sorbet-runtime gem:
|
28
|
+
# https://github.com/sorbet/sorbet/blob/master/gems/sorbet-runtime/lib/types/props/_props.rb#L31-L106
|
29
|
+
#
|
30
|
+
# @param name [String] The name of this property.
|
31
|
+
# @param type [String] A Sorbet string of this property's type, such as
|
32
|
+
# +"String"+.
|
33
|
+
# @return [void]
|
34
|
+
def initialize(name, type, optional: nil, enum: nil, dont_store: nil,
|
35
|
+
foreign: nil, default: nil, factory: nil, immutable: nil, array: nil,
|
36
|
+
override: nil, redaction: nil)
|
37
|
+
|
38
|
+
@name = name
|
39
|
+
@type = type
|
40
|
+
@optional = optional
|
41
|
+
@enum = enum
|
42
|
+
@dont_store = dont_store
|
43
|
+
@foreign = foreign
|
44
|
+
@default = default
|
45
|
+
@factory = factory
|
46
|
+
@immutable = immutable
|
47
|
+
@array = array
|
48
|
+
@override = override
|
49
|
+
@redaction = redaction
|
50
|
+
end
|
51
|
+
|
52
|
+
sig { params(other: Object).returns(T::Boolean) }
|
53
|
+
# Returns true if this instance is equal to another instance.
|
54
|
+
#
|
55
|
+
# @param other [Object] The other instance. If this is not a {StructProp} (or a
|
56
|
+
# subclass of it), this will always return false.
|
57
|
+
# @return [Boolean]
|
58
|
+
def ==(other)
|
59
|
+
StructProp === other &&
|
60
|
+
name == other.name &&
|
61
|
+
type == other.type &&
|
62
|
+
optional == other.optional &&
|
63
|
+
enum == other.enum &&
|
64
|
+
dont_store == other.dont_store &&
|
65
|
+
foreign == other.foreign &&
|
66
|
+
default == other.default &&
|
67
|
+
factory == other.factory &&
|
68
|
+
immutable == other.immutable &&
|
69
|
+
array == other.array &&
|
70
|
+
override == other.override &&
|
71
|
+
redaction == other.redaction
|
72
|
+
end
|
73
|
+
|
74
|
+
sig { returns(String) }
|
75
|
+
# The name of this parameter, including any prefixes or suffixes such as
|
76
|
+
# +*+.
|
77
|
+
# @return [String]
|
78
|
+
attr_reader :name
|
79
|
+
|
80
|
+
sig { returns(T.nilable(String)) }
|
81
|
+
# A Sorbet string of this parameter's type, such as +"String"+ or
|
82
|
+
# +"T.untyped"+.
|
83
|
+
# @return [String, nil]
|
84
|
+
attr_reader :type
|
85
|
+
|
86
|
+
sig { returns(T.nilable(T.any(T::Boolean, Symbol))) }
|
87
|
+
attr_reader :optional
|
88
|
+
|
89
|
+
sig { returns(T.nilable(String)) }
|
90
|
+
attr_reader :enum
|
91
|
+
|
92
|
+
sig { returns(T.nilable(T::Boolean)) }
|
93
|
+
attr_reader :dont_store
|
94
|
+
|
95
|
+
sig { returns(T.nilable(String)) }
|
96
|
+
attr_reader :foreign
|
97
|
+
|
98
|
+
sig { returns(T.nilable(String)) }
|
99
|
+
attr_reader :default
|
100
|
+
|
101
|
+
sig { returns(T.nilable(String)) }
|
102
|
+
attr_reader :factory
|
103
|
+
|
104
|
+
sig { returns(T.nilable(T::Boolean)) }
|
105
|
+
attr_reader :immutable
|
106
|
+
|
107
|
+
sig { returns(T.nilable(String)) }
|
108
|
+
attr_reader :array
|
109
|
+
|
110
|
+
sig { returns(T.nilable(T::Boolean)) }
|
111
|
+
attr_reader :override
|
112
|
+
|
113
|
+
sig { returns(T.nilable(String)) }
|
114
|
+
attr_reader :redaction
|
115
|
+
|
116
|
+
# The optional properties available on instances of this class.
|
117
|
+
EXTRA_PROPERTIES = %i{
|
118
|
+
optional enum dont_store foreign default factory immutable array override redaction
|
119
|
+
}
|
120
|
+
|
121
|
+
sig { returns(String) }
|
122
|
+
# Returns the +prop+ call required to create this property.
|
123
|
+
# @return [String]
|
124
|
+
def to_prop_call
|
125
|
+
call = "prop :#{name}, #{type}"
|
126
|
+
|
127
|
+
EXTRA_PROPERTIES.each do |extra_property|
|
128
|
+
value = send extra_property
|
129
|
+
call += ", #{extra_property}: #{value}" unless value.nil?
|
130
|
+
end
|
131
|
+
|
132
|
+
call
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
@@ -0,0 +1,104 @@
|
|
1
|
+
# typed: true
|
2
|
+
|
3
|
+
require 'open3'
|
4
|
+
require 'json'
|
5
|
+
|
6
|
+
module Parlour
|
7
|
+
module TypeLoader
|
8
|
+
extend T::Sig
|
9
|
+
|
10
|
+
# TODO: make this into a class which stores configuration and passes it to
|
11
|
+
# all typeparsers
|
12
|
+
|
13
|
+
sig { params(source: String, filename: T.nilable(String), generator: T.nilable(RbiGenerator)).returns(RbiGenerator::Namespace) }
|
14
|
+
# Converts Ruby source code into a tree of objects.
|
15
|
+
#
|
16
|
+
# @param [String] source The Ruby source code.
|
17
|
+
# @param [String, nil] filename The filename to use when parsing this code.
|
18
|
+
# This may be used in error messages, but is optional.
|
19
|
+
# @return [RbiGenerator::Namespace] The root of the object tree.
|
20
|
+
def self.load_source(source, filename = nil, generator: nil)
|
21
|
+
TypeParser.from_source(filename || '(source)', source, generator: generator).parse_all
|
22
|
+
end
|
23
|
+
|
24
|
+
sig { params(filename: String, generator: T.nilable(RbiGenerator)).returns(RbiGenerator::Namespace) }
|
25
|
+
# Converts Ruby source code into a tree of objects from a file.
|
26
|
+
#
|
27
|
+
# @param [String] filename The name of the file to load code from.
|
28
|
+
# @return [RbiGenerator::Namespace] The root of the object tree.
|
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)
|
40
|
+
end
|
41
|
+
# Loads an entire Sorbet project using Sorbet's file table, obeying any
|
42
|
+
# "typed: ignore" sigils, into a tree of objects.
|
43
|
+
#
|
44
|
+
# Files within sorbet/rbi/hidden-definitions are excluded, as they cause
|
45
|
+
# merging issues with abstract classes due to sorbet/sorbet#1653.
|
46
|
+
#
|
47
|
+
# @param [String] root The root of the project; where the "sorbet" directory
|
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.
|
51
|
+
# @param [Array<String>] exclusions A list of files to exclude when loading
|
52
|
+
# the project, relative to the given root.
|
53
|
+
# @return [RbiGenerator::Namespace] The root of the object tree.
|
54
|
+
def self.load_project(root, inclusions: ['.'], exclusions: [], generator: nil)
|
55
|
+
expanded_inclusions = inclusions.map { |i| File.expand_path(i, root) }
|
56
|
+
expanded_exclusions = exclusions.map { |e| File.expand_path(e, root) }
|
57
|
+
|
58
|
+
stdin, stdout, stderr, wait_thr = T.unsafe(Open3).popen3(
|
59
|
+
'bundle exec srb tc -p file-table-json',
|
60
|
+
chdir: root
|
61
|
+
)
|
62
|
+
|
63
|
+
file_table_hash = JSON.parse(T.must(stdout.read))
|
64
|
+
file_table_entries = file_table_hash['files']
|
65
|
+
|
66
|
+
namespaces = T.let([], T::Array[Parlour::RbiGenerator::Namespace])
|
67
|
+
file_table_entries.each do |file_table_entry|
|
68
|
+
next if file_table_entry['sigil'] == 'Ignore' ||
|
69
|
+
file_table_entry['strict'] == 'Ignore'
|
70
|
+
|
71
|
+
rel_path = file_table_entry['path']
|
72
|
+
next if rel_path.start_with?('./sorbet/rbi/hidden-definitions/')
|
73
|
+
path = File.expand_path(rel_path, root)
|
74
|
+
|
75
|
+
# Skip this file if it was excluded
|
76
|
+
next if !expanded_inclusions.any? { |i| path.start_with?(i) } \
|
77
|
+
|| expanded_exclusions.any? { |e| path.start_with?(e) }
|
78
|
+
|
79
|
+
# There are some entries which are URLs to stdlib
|
80
|
+
next unless File.exist?(path)
|
81
|
+
|
82
|
+
namespaces << load_file(path, generator: generator)
|
83
|
+
end
|
84
|
+
|
85
|
+
namespaces.uniq!
|
86
|
+
|
87
|
+
raise 'project is empty' if namespaces.empty?
|
88
|
+
|
89
|
+
first_namespace, *other_namespaces = namespaces
|
90
|
+
first_namespace = T.must(first_namespace)
|
91
|
+
other_namespaces = T.must(other_namespaces)
|
92
|
+
|
93
|
+
raise 'cannot merge namespaces loaded from a project' \
|
94
|
+
unless first_namespace.mergeable?(other_namespaces)
|
95
|
+
first_namespace.merge_into_self(other_namespaces)
|
96
|
+
|
97
|
+
ConflictResolver.new.resolve_conflicts(first_namespace) do |n, o|
|
98
|
+
raise "conflict of #{o.length} objects: #{n}"
|
99
|
+
end
|
100
|
+
|
101
|
+
first_namespace
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
@@ -0,0 +1,854 @@
|
|
1
|
+
# typed: true
|
2
|
+
|
3
|
+
# TODO: support sig without runtime
|
4
|
+
|
5
|
+
# Suppress versioning warnings - the majority of users will not actually be
|
6
|
+
# using this, so we don't want to pollute their console
|
7
|
+
old_verbose = $VERBOSE
|
8
|
+
begin
|
9
|
+
$VERBOSE = nil
|
10
|
+
require 'parser/current'
|
11
|
+
ensure
|
12
|
+
$VERBOSE = old_verbose
|
13
|
+
end
|
14
|
+
|
15
|
+
module Parlour
|
16
|
+
# Parses Ruby source to find Sorbet type signatures.
|
17
|
+
class TypeParser
|
18
|
+
# Represents a path of indices which can be traversed to reach a specific
|
19
|
+
# node in an AST.
|
20
|
+
class NodePath
|
21
|
+
extend T::Sig
|
22
|
+
|
23
|
+
sig { returns(T::Array[Integer]) }
|
24
|
+
# @return [Array<Integer>] The path of indices.
|
25
|
+
attr_reader :indices
|
26
|
+
|
27
|
+
sig { params(indices: T::Array[Integer]).void }
|
28
|
+
# Creates a new {NodePath}.
|
29
|
+
#
|
30
|
+
# @param [Array<Integer>] indices The path of indices.
|
31
|
+
def initialize(indices)
|
32
|
+
@indices = indices
|
33
|
+
end
|
34
|
+
|
35
|
+
sig { returns(NodePath) }
|
36
|
+
# @return [NodePath] The parent path for the node at this path.
|
37
|
+
def parent
|
38
|
+
if indices.empty?
|
39
|
+
raise IndexError, 'cannot get parent of an empty path'
|
40
|
+
else
|
41
|
+
NodePath.new(T.must(indices[0...-1]))
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
sig { params(index: Integer).returns(NodePath) }
|
46
|
+
# @param [Integer] index The index of the child whose path to return.
|
47
|
+
# @return [NodePath] The path to the child at the given index.
|
48
|
+
def child(index)
|
49
|
+
NodePath.new(indices + [index])
|
50
|
+
end
|
51
|
+
|
52
|
+
sig { params(offset: Integer).returns(NodePath) }
|
53
|
+
# @param [Integer] offset The sibling offset to use. 0 is the current
|
54
|
+
# node, -1 is the previous node, or 3 is is the node three nodes after
|
55
|
+
# this one.
|
56
|
+
# @return [NodePath] The path to the sibling with the given context.
|
57
|
+
def sibling(offset)
|
58
|
+
if indices.empty?
|
59
|
+
raise IndexError, 'cannot get sibling of an empty path'
|
60
|
+
else
|
61
|
+
*xs, x = indices
|
62
|
+
x = T.must(x)
|
63
|
+
raise ArgumentError, "sibling offset of #{offset} results in " \
|
64
|
+
"negative index of #{x + offset}" if x + offset < 0
|
65
|
+
NodePath.new(T.must(xs) + [x + offset])
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
sig { params(start: Parser::AST::Node).returns(Parser::AST::Node) }
|
70
|
+
# Follows this path of indices from an AST node.
|
71
|
+
#
|
72
|
+
# @param [Parser::AST::Node] start The AST node to start from.
|
73
|
+
# @return [Parser::AST::Node] The resulting AST node.
|
74
|
+
def traverse(start)
|
75
|
+
current = T.unsafe(start)
|
76
|
+
indices.each do |index|
|
77
|
+
raise IndexError, 'path does not exist' if index >= current.to_a.length
|
78
|
+
current = current.to_a[index]
|
79
|
+
end
|
80
|
+
current
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
extend T::Sig
|
85
|
+
|
86
|
+
sig { params(ast: Parser::AST::Node, unknown_node_errors: T::Boolean, generator: T.nilable(RbiGenerator)).void }
|
87
|
+
# Creates a new {TypeParser} from whitequark/parser AST.
|
88
|
+
#
|
89
|
+
# @param [Parser::AST::Node] The AST.
|
90
|
+
# @param [Boolean] unknown_node_errors Whether to raise an error if a node
|
91
|
+
# of an unknown kind is encountered. If false, the node is simply ignored;
|
92
|
+
# if true, a parse error is raised. Setting this to true is likely to
|
93
|
+
# raise errors for lots of non-RBI Ruby code, but setting it to false
|
94
|
+
# could miss genuine typed objects if Parlour or your code contains a bug.
|
95
|
+
def initialize(ast, unknown_node_errors: false, generator: nil)
|
96
|
+
@ast = ast
|
97
|
+
@unknown_node_errors = unknown_node_errors
|
98
|
+
@generator = generator || DetachedRbiGenerator.new
|
99
|
+
end
|
100
|
+
|
101
|
+
sig { params(filename: String, source: String, generator: T.nilable(RbiGenerator)).returns(TypeParser) }
|
102
|
+
# Creates a new {TypeParser} from a source file and its filename.
|
103
|
+
#
|
104
|
+
# @param [String] filename A filename. This does not need to be an actual
|
105
|
+
# file; it merely identifies this source.
|
106
|
+
# @param [String] source The Ruby source code.
|
107
|
+
# @return [TypeParser]
|
108
|
+
def self.from_source(filename, source, generator: nil)
|
109
|
+
buffer = Parser::Source::Buffer.new(filename)
|
110
|
+
buffer.source = source
|
111
|
+
|
112
|
+
# || special case handles parser returning nil on an empty file
|
113
|
+
parsed = Parser::CurrentRuby.new.parse(buffer) || Parser::AST::Node.new(:body)
|
114
|
+
TypeParser.new(parsed, generator: generator)
|
115
|
+
end
|
116
|
+
|
117
|
+
sig { returns(Parser::AST::Node) }
|
118
|
+
# @return [Parser::AST::Node] The AST which this type parser should use.
|
119
|
+
attr_accessor :ast
|
120
|
+
|
121
|
+
sig { returns(T::Boolean) }
|
122
|
+
# @return [Boolean] Whether to raise an error if a node of an unknown kind
|
123
|
+
# is encountered.
|
124
|
+
attr_reader :unknown_node_errors
|
125
|
+
|
126
|
+
sig { returns(RbiGenerator) }
|
127
|
+
# @return [RbiGenerator] The {RbiGenerator} to load the source into.
|
128
|
+
attr_accessor :generator
|
129
|
+
|
130
|
+
# Parses the entire source file and returns the resulting root namespace.
|
131
|
+
#
|
132
|
+
# @return [RbiGenerator::Namespace] The root namespace of the parsed source.
|
133
|
+
sig { returns(RbiGenerator::Namespace) }
|
134
|
+
def parse_all
|
135
|
+
root = generator.root
|
136
|
+
root.children.concat(parse_path_to_object(NodePath.new([])))
|
137
|
+
root
|
138
|
+
end
|
139
|
+
|
140
|
+
# Given a path to a node in the AST, parses the object definitions it
|
141
|
+
# represents and returns it, recursing to any child namespaces and parsing
|
142
|
+
# any methods within.
|
143
|
+
#
|
144
|
+
# If the node directly represents several nodes, such as being a
|
145
|
+
# (begin ...) node, they are all returned.
|
146
|
+
#
|
147
|
+
# @param [NodePath] path The path to the namespace definition. Do not pass
|
148
|
+
# any of the other parameters to this method in an external call.
|
149
|
+
# @return [Array<RbiGenerator::RbiObject>] The objects the node at the path
|
150
|
+
# represents, parsed into an RBI generator object.
|
151
|
+
sig { params(path: NodePath, is_within_eigenclass: T::Boolean).returns(T::Array[RbiGenerator::RbiObject]) }
|
152
|
+
def parse_path_to_object(path, is_within_eigenclass: false)
|
153
|
+
node = path.traverse(ast)
|
154
|
+
|
155
|
+
case node.type
|
156
|
+
when :class
|
157
|
+
parse_err 'cannot declare classes in an eigenclass', node if is_within_eigenclass
|
158
|
+
|
159
|
+
name, superclass, body = *node
|
160
|
+
final = body_has_modifier?(body, :final!)
|
161
|
+
abstract = body_has_modifier?(body, :abstract!)
|
162
|
+
includes, extends = body ? body_includes_and_extends(body) : [[], []]
|
163
|
+
|
164
|
+
# Create all classes, if we're given a definition like "class A::B"
|
165
|
+
*parent_names, this_name = constant_names(name)
|
166
|
+
target = T.let(nil, T.nilable(RbiGenerator::Namespace))
|
167
|
+
top_level = T.let(nil, T.nilable(RbiGenerator::Namespace))
|
168
|
+
parent_names.each do |n|
|
169
|
+
new_obj = RbiGenerator::Namespace.new(
|
170
|
+
generator,
|
171
|
+
n.to_s,
|
172
|
+
false,
|
173
|
+
)
|
174
|
+
target.children << new_obj if target
|
175
|
+
target = new_obj
|
176
|
+
top_level ||= new_obj
|
177
|
+
end if parent_names
|
178
|
+
|
179
|
+
# Instantiate the correct kind of class
|
180
|
+
if ['T::Struct', '::T::Struct'].include?(node_to_s(superclass))
|
181
|
+
# Find all of this struct's props and consts
|
182
|
+
# The body is typically a `begin` element but when there's only
|
183
|
+
# one node there's no wrapping block and instead it would directly
|
184
|
+
# be the node.
|
185
|
+
prop_nodes = body.nil? ? [] :
|
186
|
+
(body.type == :begin ? body.to_a : [body]).select { |x| x.type == :send && [:prop, :const].include?(x.to_a[1]) }
|
187
|
+
|
188
|
+
props = prop_nodes.map do |prop_node|
|
189
|
+
_, prop_type, name_node, type_node, extras_hash_node = *prop_node
|
190
|
+
|
191
|
+
# "const" is just "prop ..., immutable: true"
|
192
|
+
extras_hash = extras_hash_node.to_a.map do |pair_node|
|
193
|
+
key_node, value_node = *pair_node
|
194
|
+
parse_err 'prop/const key must be a symbol', prop_node unless key_node.type == :sym
|
195
|
+
key = key_node.to_a.first
|
196
|
+
|
197
|
+
value =
|
198
|
+
if key == :default
|
199
|
+
T.must(node_to_s(value_node))
|
200
|
+
else
|
201
|
+
case value_node.type
|
202
|
+
when :true
|
203
|
+
true
|
204
|
+
when :false
|
205
|
+
false
|
206
|
+
when :sym
|
207
|
+
value_node.to_a.first
|
208
|
+
else
|
209
|
+
T.must(node_to_s(value_node))
|
210
|
+
end
|
211
|
+
end
|
212
|
+
|
213
|
+
[key, value]
|
214
|
+
end.to_h
|
215
|
+
|
216
|
+
if prop_type == :const
|
217
|
+
parse_err 'const cannot use immutable key', prop_node unless extras_hash[:immutable].nil?
|
218
|
+
extras_hash[:immutable] = true
|
219
|
+
end
|
220
|
+
|
221
|
+
# Get prop/const name
|
222
|
+
parse_err 'prop/const name must be a symbol or string', prop_node unless [:sym, :str].include?(name_node.type)
|
223
|
+
name = name_node.to_a.first.to_s
|
224
|
+
|
225
|
+
RbiGenerator::StructProp.new(
|
226
|
+
name,
|
227
|
+
T.must(node_to_s(type_node)),
|
228
|
+
**T.unsafe(extras_hash)
|
229
|
+
)
|
230
|
+
end
|
231
|
+
|
232
|
+
final_obj = RbiGenerator::StructClassNamespace.new(
|
233
|
+
generator,
|
234
|
+
this_name.to_s,
|
235
|
+
final,
|
236
|
+
props,
|
237
|
+
abstract,
|
238
|
+
)
|
239
|
+
elsif ['T::Enum', '::T::Enum'].include?(node_to_s(superclass))
|
240
|
+
# Look for (block (send nil :enums) ...) structure
|
241
|
+
enums_node = body.nil? ? nil :
|
242
|
+
(body.type == :begin ? body.to_a : [body]).find { |x| x.type == :block && x.to_a[0].type == :send && x.to_a[0].to_a[1] == :enums }
|
243
|
+
|
244
|
+
# Find the constant assigments within this block
|
245
|
+
constant_nodes = enums_node.to_a[2].to_a
|
246
|
+
|
247
|
+
# Convert this to an array to enums as EnumClassNamespace expects
|
248
|
+
enums = constant_nodes.map do |constant_node|
|
249
|
+
_, name, new_node = *constant_node
|
250
|
+
serialize_value = node_to_s(new_node.to_a[2])
|
251
|
+
|
252
|
+
serialize_value ? [name.to_s, serialize_value] : name.to_s
|
253
|
+
end
|
254
|
+
|
255
|
+
final_obj = RbiGenerator::EnumClassNamespace.new(
|
256
|
+
generator,
|
257
|
+
this_name.to_s,
|
258
|
+
final,
|
259
|
+
enums,
|
260
|
+
abstract,
|
261
|
+
)
|
262
|
+
else
|
263
|
+
final_obj = RbiGenerator::ClassNamespace.new(
|
264
|
+
generator,
|
265
|
+
this_name.to_s,
|
266
|
+
final,
|
267
|
+
node_to_s(superclass),
|
268
|
+
abstract,
|
269
|
+
)
|
270
|
+
end
|
271
|
+
|
272
|
+
final_obj.children.concat(parse_path_to_object(path.child(2))) if body
|
273
|
+
final_obj.create_includes(includes)
|
274
|
+
final_obj.create_extends(extends)
|
275
|
+
|
276
|
+
if target
|
277
|
+
target.children << final_obj
|
278
|
+
[top_level]
|
279
|
+
else
|
280
|
+
[final_obj]
|
281
|
+
end
|
282
|
+
when :module
|
283
|
+
parse_err 'cannot declare modules in an eigenclass', node if is_within_eigenclass
|
284
|
+
|
285
|
+
name, body = *node
|
286
|
+
final = body_has_modifier?(body, :final!)
|
287
|
+
interface = body_has_modifier?(body, :interface!)
|
288
|
+
includes, extends = body ? body_includes_and_extends(body) : [[], []]
|
289
|
+
|
290
|
+
# Create all modules, if we're given a definition like "module A::B"
|
291
|
+
*parent_names, this_name = constant_names(name)
|
292
|
+
target = T.let(nil, T.nilable(RbiGenerator::Namespace))
|
293
|
+
top_level = T.let(nil, T.nilable(RbiGenerator::Namespace))
|
294
|
+
parent_names.each do |n|
|
295
|
+
new_obj = RbiGenerator::Namespace.new(
|
296
|
+
generator,
|
297
|
+
n.to_s,
|
298
|
+
false,
|
299
|
+
)
|
300
|
+
target.children << new_obj if target
|
301
|
+
target = new_obj
|
302
|
+
top_level ||= new_obj
|
303
|
+
end if parent_names
|
304
|
+
|
305
|
+
final_obj = RbiGenerator::ModuleNamespace.new(
|
306
|
+
generator,
|
307
|
+
this_name.to_s,
|
308
|
+
final,
|
309
|
+
interface,
|
310
|
+
) do |m|
|
311
|
+
m.children.concat(parse_path_to_object(path.child(1))) if body
|
312
|
+
m.create_includes(includes)
|
313
|
+
m.create_extends(extends)
|
314
|
+
end
|
315
|
+
|
316
|
+
if target
|
317
|
+
target.children << final_obj
|
318
|
+
[top_level]
|
319
|
+
else
|
320
|
+
[final_obj]
|
321
|
+
end
|
322
|
+
when :send, :block
|
323
|
+
if sig_node?(node)
|
324
|
+
parse_sig_into_methods(path, is_within_eigenclass: is_within_eigenclass)
|
325
|
+
elsif node.type == :send &&
|
326
|
+
[:attr_reader, :attr_writer, :attr_accessor].include?(node.to_a[1]) &&
|
327
|
+
!previous_sibling_sig_node?(path)
|
328
|
+
parse_method_into_methods(path, is_within_eigenclass: is_within_eigenclass)
|
329
|
+
else
|
330
|
+
[]
|
331
|
+
end
|
332
|
+
when :def, :defs
|
333
|
+
if previous_sibling_sig_node?(path)
|
334
|
+
[]
|
335
|
+
else
|
336
|
+
parse_method_into_methods(path, is_within_eigenclass: is_within_eigenclass)
|
337
|
+
end
|
338
|
+
when :sclass
|
339
|
+
parse_err 'cannot access eigen of non-self object', node unless node.to_a[0].type == :self
|
340
|
+
parse_path_to_object(path.child(1), is_within_eigenclass: true)
|
341
|
+
when :begin
|
342
|
+
# Just map over all the things
|
343
|
+
node.to_a.length.times.map do |c|
|
344
|
+
parse_path_to_object(path.child(c), is_within_eigenclass: is_within_eigenclass)
|
345
|
+
end.flatten
|
346
|
+
when :casgn
|
347
|
+
_, name, body = *node
|
348
|
+
[Parlour::RbiGenerator::Constant.new(
|
349
|
+
generator,
|
350
|
+
name: T.must(name).to_s,
|
351
|
+
value: T.must(node_to_s(body)),
|
352
|
+
)]
|
353
|
+
else
|
354
|
+
if unknown_node_errors
|
355
|
+
parse_err "don't understand node type #{node.type}", node
|
356
|
+
else
|
357
|
+
[]
|
358
|
+
end
|
359
|
+
end
|
360
|
+
end
|
361
|
+
|
362
|
+
# A parsed sig, not associated with a method.
|
363
|
+
class IntermediateSig < T::Struct
|
364
|
+
prop :type_parameters, T.nilable(T::Array[Symbol])
|
365
|
+
prop :overridable, T::Boolean
|
366
|
+
prop :override, T::Boolean
|
367
|
+
prop :abstract, T::Boolean
|
368
|
+
prop :final, T::Boolean
|
369
|
+
prop :return_type, T.nilable(String)
|
370
|
+
prop :params, T.nilable(T::Array[Parser::AST::Node])
|
371
|
+
end
|
372
|
+
|
373
|
+
sig { params(path: NodePath).returns(IntermediateSig) }
|
374
|
+
# Given a path to a sig in the AST, parses that sig into an intermediate
|
375
|
+
# sig object.
|
376
|
+
# This will raise an exception if the sig is invalid.
|
377
|
+
# This is intended to be called by {#parse_sig_into_methods}, and shouldn't
|
378
|
+
# be called manually unless you're doing something hacky.
|
379
|
+
#
|
380
|
+
# @param [NodePath] path The sig to parse.
|
381
|
+
# @return [IntermediateSig] The parsed sig.
|
382
|
+
def parse_sig_into_sig(path)
|
383
|
+
sig_block_node = path.traverse(ast)
|
384
|
+
|
385
|
+
# A sig's AST uses lots of nested nodes due to a deep call chain, so let's
|
386
|
+
# flatten it out to make it easier to work with
|
387
|
+
sig_chain = []
|
388
|
+
current_sig_chain_node = sig_block_node.to_a[2]
|
389
|
+
while current_sig_chain_node
|
390
|
+
name = current_sig_chain_node.to_a[1]
|
391
|
+
arguments = current_sig_chain_node.to_a[2..-1]
|
392
|
+
|
393
|
+
sig_chain << [name, arguments]
|
394
|
+
current_sig_chain_node = current_sig_chain_node.to_a[0]
|
395
|
+
end
|
396
|
+
|
397
|
+
# Get basic boolean flags
|
398
|
+
override = !!sig_chain.find { |(n, a)| n == :override && a.empty? }
|
399
|
+
overridable = !!sig_chain.find { |(n, a)| n == :overridable && a.empty? }
|
400
|
+
abstract = !!sig_chain.find { |(n, a)| n == :abstract && a.empty? }
|
401
|
+
|
402
|
+
# Determine whether this method is final (i.e. sig(:final))
|
403
|
+
_, _, *sig_arguments = *sig_block_node.to_a[0]
|
404
|
+
final = sig_arguments.any? { |a| a.type == :sym && a.to_a[0] == :final }
|
405
|
+
|
406
|
+
# Find the return type by looking for a "returns" call
|
407
|
+
return_type = sig_chain
|
408
|
+
.find { |(n, _)| n == :returns }
|
409
|
+
&.then do |(_, a)|
|
410
|
+
parse_err 'wrong number of arguments in "returns" for sig', sig_block_node if a.length != 1
|
411
|
+
node_to_s(a[0])
|
412
|
+
end
|
413
|
+
|
414
|
+
# Find the arguments specified in the "params" call in the sig
|
415
|
+
sig_args = sig_chain
|
416
|
+
.find { |(n, _)| n == :params }
|
417
|
+
&.then do |(_, a)|
|
418
|
+
parse_err 'wrong number of arguments in "params" for sig', sig_block_node if a.length != 1
|
419
|
+
arg = a[0]
|
420
|
+
parse_err 'argument to "params" should be a hash', arg unless arg.type == :hash
|
421
|
+
arg.to_a
|
422
|
+
end
|
423
|
+
|
424
|
+
# Find type parameters if they were used
|
425
|
+
type_parameters = sig_chain
|
426
|
+
.find { |(n, _)| n == :type_parameters }
|
427
|
+
&.then do |(_, a)|
|
428
|
+
a.map do |arg|
|
429
|
+
parse_err 'type parameter must be a symbol', arg if arg.type != :sym
|
430
|
+
arg.to_a[0]
|
431
|
+
end
|
432
|
+
end
|
433
|
+
|
434
|
+
IntermediateSig.new(
|
435
|
+
type_parameters: type_parameters,
|
436
|
+
overridable: overridable,
|
437
|
+
override: override,
|
438
|
+
abstract: abstract,
|
439
|
+
final: final,
|
440
|
+
params: sig_args,
|
441
|
+
return_type: return_type
|
442
|
+
)
|
443
|
+
end
|
444
|
+
|
445
|
+
sig { params(path: NodePath, is_within_eigenclass: T::Boolean).returns(T::Array[RbiGenerator::Method]) }
|
446
|
+
# Given a path to a sig in the AST, finds the associated definition and
|
447
|
+
# parses them into methods.
|
448
|
+
# This will raise an exception if the sig is invalid.
|
449
|
+
# Usually this will return one method; the only exception currently is for
|
450
|
+
# attributes, where multiple can be declared in one call, e.g.
|
451
|
+
# +attr_reader :x, :y, :z+.
|
452
|
+
#
|
453
|
+
# @param [NodePath] path The sig to parse.
|
454
|
+
# @param [Boolean] is_within_eigenclass Whether the method definition this sig is
|
455
|
+
# associated with appears inside an eigenclass definition. If true, the
|
456
|
+
# returned method is made a class method. If the method definition
|
457
|
+
# is already a class method, an exception is thrown as the method will be
|
458
|
+
# a class method of the eigenclass, which Parlour can't represent.
|
459
|
+
# @return [<RbiGenerator::Method>] The parsed methods.
|
460
|
+
def parse_sig_into_methods(path, is_within_eigenclass: false)
|
461
|
+
sig_block_node = path.traverse(ast)
|
462
|
+
|
463
|
+
# A :def node represents a definition like "def x; end"
|
464
|
+
# A :defs node represents a definition like "def self.x; end"
|
465
|
+
def_node = path.sibling(1).traverse(ast)
|
466
|
+
case def_node.type
|
467
|
+
when :def
|
468
|
+
class_method = false
|
469
|
+
def_names = [def_node.to_a[0].to_s]
|
470
|
+
def_params = def_node.to_a[1].to_a
|
471
|
+
kind = :def
|
472
|
+
when :defs
|
473
|
+
parse_err 'targeted definitions on a non-self target are not supported', def_node \
|
474
|
+
unless def_node.to_a[0].type == :self
|
475
|
+
class_method = true
|
476
|
+
def_names = [def_node.to_a[1].to_s]
|
477
|
+
def_params = def_node.to_a[2].to_a
|
478
|
+
kind = :def
|
479
|
+
when :send
|
480
|
+
target, method_name, *parameters = *def_node
|
481
|
+
|
482
|
+
parse_err 'node after a sig must be a method definition', def_node \
|
483
|
+
unless [:attr_reader, :attr_writer, :attr_accessor].include?(method_name) \
|
484
|
+
|| target != nil
|
485
|
+
|
486
|
+
parse_err 'typed attribute should have at least one name', def_node if parameters&.length == 0
|
487
|
+
|
488
|
+
kind = :attr
|
489
|
+
attr_direction = method_name.to_s.gsub('attr_', '').to_sym
|
490
|
+
def_names = T.must(parameters).map { |param| param.to_a[0].to_s }
|
491
|
+
class_method = false
|
492
|
+
else
|
493
|
+
parse_err 'node after a sig must be a method definition', def_node
|
494
|
+
end
|
495
|
+
|
496
|
+
if is_within_eigenclass
|
497
|
+
parse_err 'cannot represent multiple levels of eigenclassing', def_node if class_method
|
498
|
+
class_method = true
|
499
|
+
end
|
500
|
+
|
501
|
+
this_sig = parse_sig_into_sig(path)
|
502
|
+
params = this_sig.params
|
503
|
+
return_type = this_sig.return_type
|
504
|
+
|
505
|
+
if kind == :def
|
506
|
+
# Sorbet allows a trailing blockarg that's not in the sig
|
507
|
+
if params &&
|
508
|
+
def_params.length == params.length + 1 &&
|
509
|
+
def_params[-1].type == :blockarg
|
510
|
+
def_params = def_params[0...-1]
|
511
|
+
end
|
512
|
+
|
513
|
+
parse_err 'mismatching number of arguments in sig and def', sig_block_node \
|
514
|
+
if params && def_params.length != params.length
|
515
|
+
|
516
|
+
# sig_args will look like:
|
517
|
+
# [(pair (sym :x) <type>), (pair (sym :y) <type>), ...]
|
518
|
+
# def_params will look like:
|
519
|
+
# [(arg :x), (arg :y), ...]
|
520
|
+
parameters = params \
|
521
|
+
? zip_by(params, ->x{ x.to_a[0].to_a[0] }, def_params, ->x{ x.to_a[0] })
|
522
|
+
.map do |sig_arg, def_param|
|
523
|
+
|
524
|
+
arg_name = def_param.to_a[0]
|
525
|
+
|
526
|
+
# TODO: anonymous restarg
|
527
|
+
full_name = arg_name.to_s
|
528
|
+
full_name = "*#{arg_name}" if def_param.type == :restarg
|
529
|
+
full_name = "**#{arg_name}" if def_param.type == :kwrestarg
|
530
|
+
full_name = "#{arg_name}:" if def_param.type == :kwarg || def_param.type == :kwoptarg
|
531
|
+
full_name = "&#{arg_name}" if def_param.type == :blockarg
|
532
|
+
|
533
|
+
default = def_param.to_a[1] ? node_to_s(def_param.to_a[1]) : nil
|
534
|
+
type = node_to_s(sig_arg.to_a[1])
|
535
|
+
|
536
|
+
RbiGenerator::Parameter.new(full_name, type: type, default: default)
|
537
|
+
end
|
538
|
+
: []
|
539
|
+
|
540
|
+
# There should only be one ever here, but future-proofing anyway
|
541
|
+
def_names.map do |def_name|
|
542
|
+
RbiGenerator::Method.new(
|
543
|
+
generator,
|
544
|
+
def_name,
|
545
|
+
parameters,
|
546
|
+
return_type,
|
547
|
+
type_parameters: this_sig.type_parameters,
|
548
|
+
override: this_sig.override,
|
549
|
+
overridable: this_sig.overridable,
|
550
|
+
abstract: this_sig.abstract,
|
551
|
+
final: this_sig.final,
|
552
|
+
class_method: class_method
|
553
|
+
)
|
554
|
+
end
|
555
|
+
elsif kind == :attr
|
556
|
+
case attr_direction
|
557
|
+
when :reader, :accessor
|
558
|
+
parse_err "attr_#{attr_direction} sig should have no parameters", sig_block_node \
|
559
|
+
if params && params.length > 0
|
560
|
+
|
561
|
+
parse_err "attr_#{attr_direction} sig should have non-void return", sig_block_node \
|
562
|
+
unless return_type
|
563
|
+
|
564
|
+
attr_type = return_type
|
565
|
+
when :writer
|
566
|
+
# These are special and can only have one name
|
567
|
+
raise 'typed attr_writer can only have one name' if def_names.length > 1
|
568
|
+
|
569
|
+
def_name = def_names[0]
|
570
|
+
parse_err "attr_writer sig should take one argument with the property's name", sig_block_node \
|
571
|
+
if !params || params.length != 1 || params[0].to_a[0].to_a[0].to_s != def_name
|
572
|
+
|
573
|
+
parse_err "attr_writer sig should have non-void return", sig_block_node \
|
574
|
+
if return_type.nil?
|
575
|
+
|
576
|
+
attr_type = T.must(node_to_s(params[0].to_a[1]))
|
577
|
+
else
|
578
|
+
raise "unknown attribute direction #{attr_direction}"
|
579
|
+
end
|
580
|
+
|
581
|
+
def_names.map do |def_name|
|
582
|
+
RbiGenerator::Attribute.new(
|
583
|
+
generator,
|
584
|
+
def_name,
|
585
|
+
attr_direction,
|
586
|
+
attr_type,
|
587
|
+
class_attribute: class_method
|
588
|
+
)
|
589
|
+
end
|
590
|
+
else
|
591
|
+
raise "unknown definition kind #{kind}"
|
592
|
+
end
|
593
|
+
end
|
594
|
+
|
595
|
+
sig { params(path: NodePath, is_within_eigenclass: T::Boolean).returns(T::Array[RbiGenerator::Method]) }
|
596
|
+
# Given a path to a method in the AST, finds the associated definition and
|
597
|
+
# parses them into methods.
|
598
|
+
# Usually this will return one method; the only exception currently is for
|
599
|
+
# attributes, where multiple can be declared in one call, e.g.
|
600
|
+
# +attr_reader :x, :y, :z+.
|
601
|
+
#
|
602
|
+
# @param [NodePath] path The sig to parse.
|
603
|
+
# @param [Boolean] is_within_eigenclass Whether the method definition this sig is
|
604
|
+
# associated with appears inside an eigenclass definition. If true, the
|
605
|
+
# returned method is made a class method. If the method definition
|
606
|
+
# is already a class method, an exception is thrown as the method will be
|
607
|
+
# a class method of the eigenclass, which Parlour can't represent.
|
608
|
+
# @return [<RbiGenerator::Method>] The parsed methods.
|
609
|
+
def parse_method_into_methods(path, is_within_eigenclass: false)
|
610
|
+
# A :def node represents a definition like "def x; end"
|
611
|
+
# A :defs node represents a definition like "def self.x; end"
|
612
|
+
def_node = path.traverse(ast)
|
613
|
+
case def_node.type
|
614
|
+
when :def
|
615
|
+
class_method = false
|
616
|
+
def_names = [def_node.to_a[0].to_s]
|
617
|
+
def_params = def_node.to_a[1].to_a
|
618
|
+
kind = :def
|
619
|
+
when :defs
|
620
|
+
parse_err 'targeted definitions on a non-self target are not supported', def_node \
|
621
|
+
unless def_node.to_a[0].type == :self
|
622
|
+
class_method = true
|
623
|
+
def_names = [def_node.to_a[1].to_s]
|
624
|
+
def_params = def_node.to_a[2].to_a
|
625
|
+
kind = :def
|
626
|
+
when :send
|
627
|
+
target, method_name, *parameters = *def_node
|
628
|
+
|
629
|
+
parse_err 'node after a sig must be a method definition', def_node \
|
630
|
+
unless [:attr_reader, :attr_writer, :attr_accessor].include?(method_name) \
|
631
|
+
|| target != nil
|
632
|
+
|
633
|
+
parse_err 'typed attribute should have at least one name', def_node if parameters&.length == 0
|
634
|
+
|
635
|
+
kind = :attr
|
636
|
+
attr_direction = method_name.to_s.gsub('attr_', '').to_sym
|
637
|
+
def_names = T.must(parameters).map { |param| param.to_a[0].to_s }
|
638
|
+
class_method = false
|
639
|
+
else
|
640
|
+
parse_err 'node after a sig must be a method definition', def_node
|
641
|
+
end
|
642
|
+
|
643
|
+
if is_within_eigenclass
|
644
|
+
parse_err 'cannot represent multiple levels of eigenclassing', def_node if class_method
|
645
|
+
class_method = true
|
646
|
+
end
|
647
|
+
|
648
|
+
return_type = "T.untyped"
|
649
|
+
|
650
|
+
if kind == :def
|
651
|
+
parameters = def_params.map do |def_param|
|
652
|
+
arg_name = def_param.to_a[0]
|
653
|
+
|
654
|
+
# TODO: anonymous restarg
|
655
|
+
full_name = arg_name.to_s
|
656
|
+
full_name = "*#{arg_name}" if def_param.type == :restarg
|
657
|
+
full_name = "**#{arg_name}" if def_param.type == :kwrestarg
|
658
|
+
full_name = "#{arg_name}:" if def_param.type == :kwarg || def_param.type == :kwoptarg
|
659
|
+
full_name = "&#{arg_name}" if def_param.type == :blockarg
|
660
|
+
|
661
|
+
default = def_param.to_a[1] ? node_to_s(def_param.to_a[1]) : nil
|
662
|
+
type = nil
|
663
|
+
|
664
|
+
RbiGenerator::Parameter.new(full_name, type: type, default: default)
|
665
|
+
end
|
666
|
+
|
667
|
+
# There should only be one ever here, but future-proofing anyway
|
668
|
+
def_names.map do |def_name|
|
669
|
+
RbiGenerator::Method.new(
|
670
|
+
generator,
|
671
|
+
def_name,
|
672
|
+
parameters,
|
673
|
+
return_type,
|
674
|
+
class_method: class_method
|
675
|
+
)
|
676
|
+
end
|
677
|
+
elsif kind == :attr
|
678
|
+
case attr_direction
|
679
|
+
when :reader, :accessor, :writer
|
680
|
+
attr_type = return_type
|
681
|
+
else
|
682
|
+
raise "unknown attribute direction #{attr_direction}"
|
683
|
+
end
|
684
|
+
|
685
|
+
def_names.map do |def_name|
|
686
|
+
RbiGenerator::Attribute.new(
|
687
|
+
generator,
|
688
|
+
def_name,
|
689
|
+
attr_direction,
|
690
|
+
attr_type,
|
691
|
+
class_attribute: class_method
|
692
|
+
)
|
693
|
+
end
|
694
|
+
else
|
695
|
+
raise "unknown definition kind #{kind}"
|
696
|
+
end
|
697
|
+
end
|
698
|
+
|
699
|
+
protected
|
700
|
+
|
701
|
+
sig { params(node: T.nilable(Parser::AST::Node)).returns(T::Array[Symbol]) }
|
702
|
+
# Given a node representing a simple chain of constants (such as A or
|
703
|
+
# A::B::C), converts that node into an array of the constant names which
|
704
|
+
# are accessed. For example, A::B::C would become [:A, :B, :C].
|
705
|
+
#
|
706
|
+
# @param [Parser::AST::Node, nil] node The node to convert. This must
|
707
|
+
# consist only of nested (:const) nodes.
|
708
|
+
# @return [Array<Symbol>] The chain of constant names.
|
709
|
+
def constant_names(node)
|
710
|
+
node ? constant_names(node.to_a[0]) + [node.to_a[1]] : []
|
711
|
+
end
|
712
|
+
|
713
|
+
sig { params(node: Parser::AST::Node).returns(T::Boolean) }
|
714
|
+
# Given a node, returns a boolean indicating whether that node represents a
|
715
|
+
# a call to "sig" with a block. No further semantic checking, such as
|
716
|
+
# whether it preceeds a method call, is done.
|
717
|
+
#
|
718
|
+
# @param [Parser::AST::Node] node The node to check.
|
719
|
+
# @return [Boolean] True if that node represents a "sig" call, false
|
720
|
+
# otherwise.
|
721
|
+
def sig_node?(node)
|
722
|
+
node.type == :block &&
|
723
|
+
node.to_a[0].type == :send &&
|
724
|
+
node.to_a[0].to_a[1] == :sig
|
725
|
+
end
|
726
|
+
|
727
|
+
sig { params(path: NodePath).returns(T::Boolean) }
|
728
|
+
# Given a path, returns a boolean indicating whether the previous sibling
|
729
|
+
# represents a call to "sig" with a block.
|
730
|
+
#
|
731
|
+
# @param [NodePath] path The path to the namespace definition.
|
732
|
+
# @return [Boolean] True if that node represents a "sig" call, false
|
733
|
+
# otherwise.
|
734
|
+
def previous_sibling_sig_node?(path)
|
735
|
+
previous_sibling = path.sibling(-1)
|
736
|
+
previous_node = previous_sibling.traverse(ast)
|
737
|
+
sig_node?(previous_node)
|
738
|
+
rescue IndexError, ArgumentError, TypeError
|
739
|
+
# `sibling` call could raise IndexError or ArgumentError if reaching into negative indices
|
740
|
+
# `traverse` call could raise TypeError if path doesn't return Parser::AST::Node
|
741
|
+
|
742
|
+
false
|
743
|
+
end
|
744
|
+
|
745
|
+
sig { params(node: T.nilable(Parser::AST::Node)).returns(T.nilable(String)) }
|
746
|
+
# Given an AST node, returns the source code from which it was constructed.
|
747
|
+
# If the given AST node is nil, this returns nil.
|
748
|
+
#
|
749
|
+
# @param [Parser::AST::Node, nil] node The AST node, or nil.
|
750
|
+
# @return [String] The source code string it represents, or nil.
|
751
|
+
def node_to_s(node)
|
752
|
+
return nil unless node
|
753
|
+
|
754
|
+
exp = node.loc.expression
|
755
|
+
exp.source_buffer.source[exp.begin_pos...exp.end_pos]
|
756
|
+
end
|
757
|
+
|
758
|
+
sig { params(node: T.nilable(Parser::AST::Node), modifier: Symbol).returns(T::Boolean) }
|
759
|
+
# Given an AST node and a symbol, determines if that node is a call (or a
|
760
|
+
# body containing a call at the top level) to the method represented by the
|
761
|
+
# symbol, without any arguments or a block.
|
762
|
+
#
|
763
|
+
# This is designed to be used to determine if a namespace body uses a Sorbet
|
764
|
+
# modifier such as "abstract!".
|
765
|
+
#
|
766
|
+
# @param [Parser::AST::Node, nil] node The AST node to search in.
|
767
|
+
# @param [Symbol] modifier The method name to search for.
|
768
|
+
# @return [T::Boolean] True if the call is found, or false otherwise.
|
769
|
+
def body_has_modifier?(node, modifier)
|
770
|
+
return false unless node
|
771
|
+
|
772
|
+
(node.type == :send && node.to_a == [nil, modifier]) ||
|
773
|
+
(node.type == :begin &&
|
774
|
+
node.to_a.any? { |c| c.type == :send && c.to_a == [nil, modifier] })
|
775
|
+
end
|
776
|
+
|
777
|
+
sig { params(node: Parser::AST::Node).returns([T::Array[String], T::Array[String]]) }
|
778
|
+
# Given an AST node representing the body of a class or module, returns two
|
779
|
+
# arrays of the includes and extends contained within the body.
|
780
|
+
#
|
781
|
+
# @param [Parser::AST::Node] node The body of the namespace.
|
782
|
+
# @return [(Array<String>, Array<String>)] An array of the includes and an
|
783
|
+
# array of the extends.
|
784
|
+
def body_includes_and_extends(node)
|
785
|
+
result = [[], []]
|
786
|
+
|
787
|
+
nodes_to_search = node.type == :begin ? node.to_a : [node]
|
788
|
+
nodes_to_search.each do |this_node|
|
789
|
+
next unless this_node.type == :send
|
790
|
+
target, name, *args = *this_node
|
791
|
+
next unless target.nil? && args.length == 1
|
792
|
+
|
793
|
+
if name == :include
|
794
|
+
result[0] << node_to_s(args.first)
|
795
|
+
elsif name == :extend
|
796
|
+
result[1] << node_to_s(args.first)
|
797
|
+
end
|
798
|
+
end
|
799
|
+
|
800
|
+
result
|
801
|
+
end
|
802
|
+
|
803
|
+
sig { params(desc: String, node: T.any(Parser::AST::Node, NodePath)).returns(T.noreturn) }
|
804
|
+
# Raises a parse error on a node.
|
805
|
+
# @param [String] desc A description of the error.
|
806
|
+
# @param [Parser::AST::Node, NodePath] A node, passed as either a path or a
|
807
|
+
# raw parser node.
|
808
|
+
def parse_err(desc, node)
|
809
|
+
node = node.traverse(ast) if node.is_a?(NodePath)
|
810
|
+
range = node.loc.expression
|
811
|
+
buffer = range.source_buffer
|
812
|
+
|
813
|
+
raise ParseError.new(buffer, range), desc
|
814
|
+
end
|
815
|
+
|
816
|
+
sig do
|
817
|
+
type_parameters(:A, :B)
|
818
|
+
.params(
|
819
|
+
a: T::Array[T.type_parameter(:A)],
|
820
|
+
fa: T.proc.params(item: T.type_parameter(:A)).returns(T.untyped),
|
821
|
+
b: T::Array[T.type_parameter(:B)],
|
822
|
+
fb: T.proc.params(item: T.type_parameter(:B)).returns(T.untyped)
|
823
|
+
)
|
824
|
+
.returns(T::Array[[T.type_parameter(:A), T.type_parameter(:B)]])
|
825
|
+
end
|
826
|
+
# Given two arrays and functions to get a key for each item in the two
|
827
|
+
# arrays, joins the two arrays into one array of pairs by that key.
|
828
|
+
#
|
829
|
+
# The arrays should both be the same length, and the key functions should
|
830
|
+
# never return duplicate keys for two different items.
|
831
|
+
#
|
832
|
+
# @param [Array<A>] a The first array.
|
833
|
+
# @param [A -> Any] fa A function to obtain a key for any element in the
|
834
|
+
# first array.
|
835
|
+
# @param [Array<B>] b The second array.
|
836
|
+
# @param [B -> Any] fb A function to obtain a key for any element in the
|
837
|
+
# second array.
|
838
|
+
# @return [Array<(A, B)>] An array of pairs, where the left of the pair is
|
839
|
+
# an element from A and the right is the element from B with the
|
840
|
+
# corresponding key.
|
841
|
+
def zip_by(a, fa, b, fb)
|
842
|
+
raise ArgumentError, "arrays are not the same length" if a.length != b.length
|
843
|
+
|
844
|
+
a.map do |a_item|
|
845
|
+
a_key = fa.(a_item)
|
846
|
+
b_items = b.select { |b_item| fb.(b_item) == a_key }
|
847
|
+
raise "multiple items for key #{a_key}" if b_items.length > 1
|
848
|
+
raise "no item in second list corresponding to key #{a_key}" if b_items.length == 0
|
849
|
+
|
850
|
+
[a_item, T.must(b_items[0])]
|
851
|
+
end
|
852
|
+
end
|
853
|
+
end
|
854
|
+
end
|