parlour 2.1.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.travis.yml +3 -4
- data/CHANGELOG.md +13 -0
- data/lib/parlour.rb +2 -0
- data/lib/parlour/conflict_resolver.rb +47 -21
- data/lib/parlour/rbi_generator/enum_class_namespace.rb +24 -2
- data/lib/parlour/rbi_generator/namespace.rb +29 -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_parser.rb +236 -17
- data/lib/parlour/version.rb +1 -1
- data/parlour.gemspec +1 -1
- metadata +8 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 1e2b9021c22592632f30a0a51c17cfbf3838c18061638297e9437c1dd34b5c9f
|
4
|
+
data.tar.gz: 337e2ea6d8212e6c2d16d2f8f5577b16bfd1c8ffa41197fad70f9dc7c51620b5
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 22e0196918f8214827f9904e45fff8689f4609ec71683ad4aca36c8eb75593e19f5d0ec4e5a5e034afdca1d3bdf7f0d9482bfb24b0e3e667553c13b5cb5b65a6
|
7
|
+
data.tar.gz: 2b322d3dcdce74985b2104a8d8beb8ac0a7b6b4de1bcc8af7288d5334d715765285cba032017c7ab6e5dccde045b284dd8056e57515e13f32f319de26fdc0666
|
data/.travis.yml
CHANGED
@@ -10,10 +10,6 @@ rvm:
|
|
10
10
|
- 2.6
|
11
11
|
- 2.7
|
12
12
|
- ruby-head
|
13
|
-
matrix:
|
14
|
-
allow_failures:
|
15
|
-
- rvm: 2.3
|
16
|
-
- rvm: ruby-head
|
17
13
|
|
18
14
|
jobs:
|
19
15
|
include:
|
@@ -27,3 +23,6 @@ jobs:
|
|
27
23
|
keep_history: true
|
28
24
|
on:
|
29
25
|
branch: master
|
26
|
+
allow_failures:
|
27
|
+
- rvm: 2.3
|
28
|
+
- rvm: ruby-head
|
data/CHANGELOG.md
CHANGED
@@ -3,6 +3,19 @@ All notable changes to this project will be documented in this file.
|
|
3
3
|
|
4
4
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
5
5
|
|
6
|
+
## [3.0.0] - 2020-05-15
|
7
|
+
### Added
|
8
|
+
- `T::Struct` classes can now be generated and parsed.
|
9
|
+
- `T::Enum` classes can now be parsed.
|
10
|
+
- Constants are now parsed.
|
11
|
+
- `TypeParser` now detects and parses methods which do not have a `sig`.
|
12
|
+
**Potentially breaking if there is a strict set of methods you are expecting Parlour to detect.**
|
13
|
+
|
14
|
+
### Fixed
|
15
|
+
- "Specialized" classes, such as enums and now structs, have had many erroneous
|
16
|
+
conflicts with standard classes or namespaces fixed.
|
17
|
+
- Attributes writers and methods with the same name no longer conflict incorrectly.
|
18
|
+
|
6
19
|
## [2.1.0] - 2020-03-22
|
7
20
|
### Added
|
8
21
|
- Files can now be excluded from the `TypeLoader`.
|
data/lib/parlour.rb
CHANGED
@@ -22,6 +22,8 @@ require 'parlour/rbi_generator/namespace'
|
|
22
22
|
require 'parlour/rbi_generator/module_namespace'
|
23
23
|
require 'parlour/rbi_generator/class_namespace'
|
24
24
|
require 'parlour/rbi_generator/enum_class_namespace'
|
25
|
+
require 'parlour/rbi_generator/struct_prop'
|
26
|
+
require 'parlour/rbi_generator/struct_class_namespace'
|
25
27
|
require 'parlour/rbi_generator'
|
26
28
|
require 'parlour/detached_rbi_generator'
|
27
29
|
|
@@ -1,4 +1,6 @@
|
|
1
1
|
# typed: true
|
2
|
+
require 'set'
|
3
|
+
|
2
4
|
module Parlour
|
3
5
|
# Responsible for resolving conflicts (that is, multiple definitions with the
|
4
6
|
# same name) between objects defined in the same namespace.
|
@@ -42,8 +44,15 @@ module Parlour
|
|
42
44
|
Debugging.debug_puts(self, Debugging::Tree.begin("Resolving conflicts for #{namespace.name}..."))
|
43
45
|
|
44
46
|
# Check for multiple definitions with the same name
|
45
|
-
|
46
|
-
|
47
|
+
# (Special case here: writer attributes get an "=" appended to their name)
|
48
|
+
grouped_by_name_children = namespace.children.group_by do |child|
|
49
|
+
if RbiGenerator::Attribute === child && child.kind == :writer
|
50
|
+
"#{child.name}=" unless child.name.end_with?('=')
|
51
|
+
else
|
52
|
+
child.name
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
47
56
|
grouped_by_name_children.each do |name, children|
|
48
57
|
Debugging.debug_puts(self, Debugging::Tree.begin("Checking children named #{name}..."))
|
49
58
|
|
@@ -60,6 +69,20 @@ module Parlour
|
|
60
69
|
next
|
61
70
|
end
|
62
71
|
|
72
|
+
# Special case: if we remove the namespaces, is everything either an
|
73
|
+
# include or an extend? If so, do nothing - this is fine
|
74
|
+
if children \
|
75
|
+
.reject { |c| c.is_a?(RbiGenerator::Namespace) }
|
76
|
+
.then do |x|
|
77
|
+
!x.empty? && x.all? do |c|
|
78
|
+
c.is_a?(RbiGenerator::Include) || c.is_a?(RbiGenerator::Extend)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
Debugging.debug_puts(self, Debugging::Tree.end("Includes/extends do not conflict with namespaces; no resolution required"))
|
83
|
+
next
|
84
|
+
end
|
85
|
+
|
63
86
|
# Special case: do we have two attributes, one of which is a class
|
64
87
|
# attribute and the other isn't? If so, do nothing - this is fine
|
65
88
|
if children.length == 2 &&
|
@@ -117,14 +140,25 @@ module Parlour
|
|
117
140
|
choice = resolver.call("Non-namespace item in a differing namespace conflict is not a single method", non_namespaces)
|
118
141
|
non_namespaces = []
|
119
142
|
non_namespaces << choice if choice
|
120
|
-
end
|
143
|
+
end
|
121
144
|
end
|
122
145
|
|
123
146
|
non_namespaces.each do |x|
|
124
147
|
namespace.children << x
|
125
148
|
end
|
126
149
|
|
127
|
-
|
150
|
+
# For certain namespace types the order matters. For example, if there's
|
151
|
+
# both a `Namespace` and `ModuleNamespace` then merging the two would
|
152
|
+
# produce different results depending on which is first.
|
153
|
+
first_index = (
|
154
|
+
namespaces.find_index { |x| RbiGenerator::EnumClassNamespace === x || RbiGenerator::StructClassNamespace === x } ||
|
155
|
+
namespaces.find_index { |x| RbiGenerator::ClassNamespace === x } ||
|
156
|
+
namespaces.find_index { |x| RbiGenerator::ModuleNamespace === x } ||
|
157
|
+
0
|
158
|
+
)
|
159
|
+
|
160
|
+
first = namespaces.delete_at(first_index)
|
161
|
+
rest = namespaces
|
128
162
|
else
|
129
163
|
raise 'unknown merge strategy; this is a Parlour bug'
|
130
164
|
end
|
@@ -161,16 +195,16 @@ module Parlour
|
|
161
195
|
|
162
196
|
sig { params(arr: T::Array[T.untyped]).returns(T.nilable(Symbol)) }
|
163
197
|
# Given an array, if all elements in the array are instances of the exact
|
164
|
-
# same class or are otherwise mergeable (for example Namespace and
|
198
|
+
# same class or are otherwise mergeable (for example Namespace and
|
165
199
|
# ClassNamespace), returns the kind of merge which needs to be made. A
|
166
200
|
# return value of nil indicates that the values cannot be merged.
|
167
201
|
#
|
168
202
|
# The following kinds are available:
|
169
203
|
# - They are all the same. (:normal)
|
170
|
-
# - There are exactly two types, one of which is Namespace and other is a
|
204
|
+
# - There are exactly two types, one of which is Namespace and other is a
|
171
205
|
# subclass of it. (:differing_namespaces)
|
172
206
|
# - One of them is Namespace or a subclass (or both, as described above),
|
173
|
-
# and the only other is Method. (also :differing_namespaces)
|
207
|
+
# and the only other is Method. (also :differing_namespaces)
|
174
208
|
#
|
175
209
|
# @param arr [Array] The array.
|
176
210
|
# @return [Symbol] The merge strategy to use, or nil if they can't be
|
@@ -182,21 +216,13 @@ module Parlour
|
|
182
216
|
|
183
217
|
# Find all the namespaces and non-namespaces
|
184
218
|
namespace_types, non_namespace_types = array_types.partition { |x| x <= RbiGenerator::Namespace }
|
219
|
+
exactly_namespace, namespace_subclasses = namespace_types.partition { |x| x == RbiGenerator::Namespace }
|
220
|
+
|
221
|
+
return nil unless namespace_subclasses.empty? \
|
222
|
+
|| (namespace_subclasses.length == 1 && namespace_subclasses.first < RbiGenerator::Namespace) \
|
223
|
+
|| namespace_subclasses.to_set == Set[RbiGenerator::ClassNamespace, RbiGenerator::StructClassNamespace] \
|
224
|
+
|| namespace_subclasses.to_set == Set[RbiGenerator::ClassNamespace, RbiGenerator::EnumClassNamespace]
|
185
225
|
|
186
|
-
# If there are two namespace types, one should be Namespace and the other
|
187
|
-
# should be a subclass of it
|
188
|
-
if namespace_types.length == 2
|
189
|
-
exactly_namespace, exactly_one_subclass = namespace_types.partition { |x| x == RbiGenerator::Namespace }
|
190
|
-
|
191
|
-
return nil unless exactly_namespace.length == 1 \
|
192
|
-
&& exactly_one_subclass.length == 1 \
|
193
|
-
&& exactly_one_subclass.first < RbiGenerator::Namespace
|
194
|
-
elsif namespace_types.length != 1
|
195
|
-
# The only other valid number of namespaces is 1, where we don't need to
|
196
|
-
# check anything
|
197
|
-
return nil
|
198
|
-
end
|
199
|
-
|
200
226
|
# It's OK, albeit cursed, for there to be a method with the same name as
|
201
227
|
# a namespace (Rainbow does this)
|
202
228
|
return nil if non_namespace_types.length != 0 && non_namespace_types != [RbiGenerator::Method]
|
@@ -80,10 +80,32 @@ module Parlour
|
|
80
80
|
# @param others [Array<RbiGenerator::RbiObject>] An array of other {EnumClassNamespace} instances.
|
81
81
|
# @return [Boolean] Whether this instance may be merged with them.
|
82
82
|
def mergeable?(others)
|
83
|
-
others = T.cast(others, T::Array[
|
83
|
+
others = T.cast(others, T::Array[Namespace]) rescue (return false)
|
84
84
|
all = others + [self]
|
85
|
+
all_enums = T.cast(all.select { |x| EnumClassNamespace === x }, T::Array[EnumClassNamespace])
|
85
86
|
|
86
|
-
T.must(super &&
|
87
|
+
T.must(super && all_enums.map { |e| e.enums.sort }.reject(&:empty?).uniq.length <= 1)
|
88
|
+
end
|
89
|
+
|
90
|
+
sig do
|
91
|
+
override.params(
|
92
|
+
others: T::Array[RbiGenerator::RbiObject]
|
93
|
+
).void
|
94
|
+
end
|
95
|
+
# Given an array of {EnumClassNamespace} instances, merges them into this one.
|
96
|
+
# You MUST ensure that {mergeable?} is true for those instances.
|
97
|
+
#
|
98
|
+
# @param others [Array<RbiGenerator::RbiObject>] An array of other {EnumClassNamespace} instances.
|
99
|
+
# @return [void]
|
100
|
+
def merge_into_self(others)
|
101
|
+
super
|
102
|
+
|
103
|
+
others.each do |other|
|
104
|
+
next unless EnumClassNamespace === other
|
105
|
+
other = T.cast(other, EnumClassNamespace)
|
106
|
+
|
107
|
+
@enums = other.enums if enums.empty?
|
108
|
+
end
|
87
109
|
end
|
88
110
|
end
|
89
111
|
end
|
@@ -196,6 +196,35 @@ module Parlour
|
|
196
196
|
new_enum_class
|
197
197
|
end
|
198
198
|
|
199
|
+
sig do
|
200
|
+
params(
|
201
|
+
name: String,
|
202
|
+
final: T::Boolean,
|
203
|
+
props: T.nilable(T::Array[StructProp]),
|
204
|
+
abstract: T::Boolean,
|
205
|
+
block: T.nilable(T.proc.params(x: StructClassNamespace).void)
|
206
|
+
).returns(StructClassNamespace)
|
207
|
+
end
|
208
|
+
# Creates a new struct class definition as a child of this namespace.
|
209
|
+
#
|
210
|
+
# @example Create a person struct.
|
211
|
+
# namespace.create_class('Person', props: [
|
212
|
+
# Parlour::RbiGenerator::StructProp.new('name', 'String')
|
213
|
+
# ])
|
214
|
+
#
|
215
|
+
# @param name [String] The name of this class.
|
216
|
+
# @param final [Boolean] Whether this namespace is final.
|
217
|
+
# @param props [Array<StructProp>] The props of the struct.
|
218
|
+
# @param abstract [Boolean] A boolean indicating whether this class is abstract.
|
219
|
+
# @param block A block which the new instance yields itself to.
|
220
|
+
# @return [EnumClassNamespace]
|
221
|
+
def create_struct_class(name, final: false, props: nil, abstract: false, &block)
|
222
|
+
new_struct_class = StructClassNamespace.new(generator, name, final, props || [], abstract, &block)
|
223
|
+
move_next_comments(new_struct_class)
|
224
|
+
children << new_struct_class
|
225
|
+
new_struct_class
|
226
|
+
end
|
227
|
+
|
199
228
|
sig do
|
200
229
|
params(
|
201
230
|
name: String,
|
@@ -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
|
data/lib/parlour/type_parser.rb
CHANGED
@@ -108,7 +108,9 @@ module Parlour
|
|
108
108
|
buffer = Parser::Source::Buffer.new(filename)
|
109
109
|
buffer.source = source
|
110
110
|
|
111
|
-
|
111
|
+
# || special case handles parser returning nil on an empty file
|
112
|
+
parsed = Parser::CurrentRuby.new.parse(buffer) || Parser::AST::Node.new(:body)
|
113
|
+
TypeParser.new(parsed)
|
112
114
|
end
|
113
115
|
|
114
116
|
sig { returns(Parser::AST::Node) }
|
@@ -169,18 +171,103 @@ module Parlour
|
|
169
171
|
top_level ||= new_obj
|
170
172
|
end if parent_names
|
171
173
|
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
174
|
+
# Instantiate the correct kind of class
|
175
|
+
if ['T::Struct', '::T::Struct'].include?(node_to_s(superclass))
|
176
|
+
# Find all of this struct's props and consts
|
177
|
+
# The body is typically a `begin` element but when there's only
|
178
|
+
# one node there's no wrapping block and instead it would directly
|
179
|
+
# be the node.
|
180
|
+
prop_nodes = body.nil? ? [] :
|
181
|
+
(body.type == :begin ? body.to_a : [body]).select { |x| x.type == :send && [:prop, :const].include?(x.to_a[1]) }
|
182
|
+
|
183
|
+
props = prop_nodes.map do |prop_node|
|
184
|
+
_, prop_type, name_node, type_node, extras_hash_node = *prop_node
|
185
|
+
|
186
|
+
# "const" is just "prop ..., immutable: true"
|
187
|
+
extras_hash = extras_hash_node.to_a.map do |pair_node|
|
188
|
+
key_node, value_node = *pair_node
|
189
|
+
parse_err 'prop/const key must be a symbol', prop_node unless key_node.type == :sym
|
190
|
+
key = key_node.to_a.first
|
191
|
+
|
192
|
+
value =
|
193
|
+
if key == :default
|
194
|
+
T.must(node_to_s(value_node))
|
195
|
+
else
|
196
|
+
case value_node.type
|
197
|
+
when :true
|
198
|
+
true
|
199
|
+
when :false
|
200
|
+
false
|
201
|
+
when :sym
|
202
|
+
value_node.to_a.first
|
203
|
+
else
|
204
|
+
T.must(node_to_s(value_node))
|
205
|
+
end
|
206
|
+
end
|
207
|
+
|
208
|
+
[key, value]
|
209
|
+
end.to_h
|
210
|
+
|
211
|
+
if prop_type == :const
|
212
|
+
parse_err 'const cannot use immutable key', prop_node unless extras_hash[:immutable].nil?
|
213
|
+
extras_hash[:immutable] = true
|
214
|
+
end
|
215
|
+
|
216
|
+
# Get prop/const name
|
217
|
+
parse_err 'prop/const name must be a symbol or string', prop_node unless [:sym, :str].include?(name_node.type)
|
218
|
+
name = name_node.to_a.first.to_s
|
219
|
+
|
220
|
+
RbiGenerator::StructProp.new(
|
221
|
+
name,
|
222
|
+
T.must(node_to_s(type_node)),
|
223
|
+
**T.unsafe(extras_hash)
|
224
|
+
)
|
225
|
+
end
|
226
|
+
|
227
|
+
final_obj = RbiGenerator::StructClassNamespace.new(
|
228
|
+
DetachedRbiGenerator.new,
|
229
|
+
this_name.to_s,
|
230
|
+
final,
|
231
|
+
props,
|
232
|
+
abstract,
|
233
|
+
)
|
234
|
+
elsif ['T::Enum', '::T::Enum'].include?(node_to_s(superclass))
|
235
|
+
# Look for (block (send nil :enums) ...) structure
|
236
|
+
enums_node = body.nil? ? nil :
|
237
|
+
(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 }
|
238
|
+
|
239
|
+
# Find the constant assigments within this block
|
240
|
+
constant_nodes = enums_node.to_a[2].to_a
|
241
|
+
|
242
|
+
# Convert this to an array to enums as EnumClassNamespace expects
|
243
|
+
enums = constant_nodes.map do |constant_node|
|
244
|
+
_, name, new_node = *constant_node
|
245
|
+
serialize_value = node_to_s(new_node.to_a[2])
|
246
|
+
|
247
|
+
serialize_value ? [name.to_s, serialize_value] : name.to_s
|
248
|
+
end
|
249
|
+
|
250
|
+
final_obj = RbiGenerator::EnumClassNamespace.new(
|
251
|
+
DetachedRbiGenerator.new,
|
252
|
+
this_name.to_s,
|
253
|
+
final,
|
254
|
+
enums,
|
255
|
+
abstract,
|
256
|
+
)
|
257
|
+
else
|
258
|
+
final_obj = RbiGenerator::ClassNamespace.new(
|
259
|
+
DetachedRbiGenerator.new,
|
260
|
+
this_name.to_s,
|
261
|
+
final,
|
262
|
+
node_to_s(superclass),
|
263
|
+
abstract,
|
264
|
+
)
|
182
265
|
end
|
183
266
|
|
267
|
+
final_obj.children.concat(parse_path_to_object(path.child(2))) if body
|
268
|
+
final_obj.create_includes(includes)
|
269
|
+
final_obj.create_extends(extends)
|
270
|
+
|
184
271
|
if target
|
185
272
|
target.children << final_obj
|
186
273
|
[top_level]
|
@@ -230,16 +317,19 @@ module Parlour
|
|
230
317
|
when :send, :block
|
231
318
|
if sig_node?(node)
|
232
319
|
parse_sig_into_methods(path, is_within_eigenclass: is_within_eigenclass)
|
320
|
+
elsif node.type == :send &&
|
321
|
+
[:attr_reader, :attr_writer, :attr_accessor].include?(node.to_a[1]) &&
|
322
|
+
!previous_sibling_sig_node?(path)
|
323
|
+
parse_method_into_methods(path, is_within_eigenclass: is_within_eigenclass)
|
233
324
|
else
|
234
325
|
[]
|
235
326
|
end
|
236
327
|
when :def, :defs
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
[]
|
328
|
+
if previous_sibling_sig_node?(path)
|
329
|
+
[]
|
330
|
+
else
|
331
|
+
parse_method_into_methods(path, is_within_eigenclass: is_within_eigenclass)
|
332
|
+
end
|
243
333
|
when :sclass
|
244
334
|
parse_err 'cannot access eigen of non-self object', node unless node.to_a[0].type == :self
|
245
335
|
parse_path_to_object(path.child(1), is_within_eigenclass: true)
|
@@ -248,6 +338,13 @@ module Parlour
|
|
248
338
|
node.to_a.length.times.map do |c|
|
249
339
|
parse_path_to_object(path.child(c), is_within_eigenclass: is_within_eigenclass)
|
250
340
|
end.flatten
|
341
|
+
when :casgn
|
342
|
+
_, name, body = *node
|
343
|
+
[Parlour::RbiGenerator::Constant.new(
|
344
|
+
DetachedRbiGenerator.new,
|
345
|
+
name: T.must(name).to_s,
|
346
|
+
value: T.must(node_to_s(body)),
|
347
|
+
)]
|
251
348
|
else
|
252
349
|
if unknown_node_errors
|
253
350
|
parse_err "don't understand node type #{node.type}", node
|
@@ -490,6 +587,110 @@ module Parlour
|
|
490
587
|
end
|
491
588
|
end
|
492
589
|
|
590
|
+
sig { params(path: NodePath, is_within_eigenclass: T::Boolean).returns(T::Array[RbiGenerator::Method]) }
|
591
|
+
# Given a path to a method in the AST, finds the associated definition and
|
592
|
+
# parses them into methods.
|
593
|
+
# Usually this will return one method; the only exception currently is for
|
594
|
+
# attributes, where multiple can be declared in one call, e.g.
|
595
|
+
# +attr_reader :x, :y, :z+.
|
596
|
+
#
|
597
|
+
# @param [NodePath] path The sig to parse.
|
598
|
+
# @param [Boolean] is_within_eigenclass Whether the method definition this sig is
|
599
|
+
# associated with appears inside an eigenclass definition. If true, the
|
600
|
+
# returned method is made a class method. If the method definition
|
601
|
+
# is already a class method, an exception is thrown as the method will be
|
602
|
+
# a class method of the eigenclass, which Parlour can't represent.
|
603
|
+
# @return [<RbiGenerator::Method>] The parsed methods.
|
604
|
+
def parse_method_into_methods(path, is_within_eigenclass: false)
|
605
|
+
# A :def node represents a definition like "def x; end"
|
606
|
+
# A :defs node represents a definition like "def self.x; end"
|
607
|
+
def_node = path.traverse(ast)
|
608
|
+
case def_node.type
|
609
|
+
when :def
|
610
|
+
class_method = false
|
611
|
+
def_names = [def_node.to_a[0].to_s]
|
612
|
+
def_params = def_node.to_a[1].to_a
|
613
|
+
kind = :def
|
614
|
+
when :defs
|
615
|
+
parse_err 'targeted definitions on a non-self target are not supported', def_node \
|
616
|
+
unless def_node.to_a[0].type == :self
|
617
|
+
class_method = true
|
618
|
+
def_names = [def_node.to_a[1].to_s]
|
619
|
+
def_params = def_node.to_a[2].to_a
|
620
|
+
kind = :def
|
621
|
+
when :send
|
622
|
+
target, method_name, *parameters = *def_node
|
623
|
+
|
624
|
+
parse_err 'node after a sig must be a method definition', def_node \
|
625
|
+
unless [:attr_reader, :attr_writer, :attr_accessor].include?(method_name) \
|
626
|
+
|| target != nil
|
627
|
+
|
628
|
+
parse_err 'typed attribute should have at least one name', def_node if parameters&.length == 0
|
629
|
+
|
630
|
+
kind = :attr
|
631
|
+
attr_direction = method_name.to_s.gsub('attr_', '').to_sym
|
632
|
+
def_names = T.must(parameters).map { |param| param.to_a[0].to_s }
|
633
|
+
class_method = false
|
634
|
+
else
|
635
|
+
parse_err 'node after a sig must be a method definition', def_node
|
636
|
+
end
|
637
|
+
|
638
|
+
if is_within_eigenclass
|
639
|
+
parse_err 'cannot represent multiple levels of eigenclassing', def_node if class_method
|
640
|
+
class_method = true
|
641
|
+
end
|
642
|
+
|
643
|
+
return_type = "T.untyped"
|
644
|
+
|
645
|
+
if kind == :def
|
646
|
+
parameters = def_params.map do |def_param|
|
647
|
+
arg_name = def_param.to_a[0]
|
648
|
+
|
649
|
+
# TODO: anonymous restarg
|
650
|
+
full_name = arg_name.to_s
|
651
|
+
full_name = "*#{arg_name}" if def_param.type == :restarg
|
652
|
+
full_name = "**#{arg_name}" if def_param.type == :kwrestarg
|
653
|
+
full_name = "#{arg_name}:" if def_param.type == :kwarg || def_param.type == :kwoptarg
|
654
|
+
full_name = "&#{arg_name}" if def_param.type == :blockarg
|
655
|
+
|
656
|
+
default = def_param.to_a[1] ? node_to_s(def_param.to_a[1]) : nil
|
657
|
+
type = nil
|
658
|
+
|
659
|
+
RbiGenerator::Parameter.new(full_name, type: type, default: default)
|
660
|
+
end
|
661
|
+
|
662
|
+
# There should only be one ever here, but future-proofing anyway
|
663
|
+
def_names.map do |def_name|
|
664
|
+
RbiGenerator::Method.new(
|
665
|
+
DetachedRbiGenerator.new,
|
666
|
+
def_name,
|
667
|
+
parameters,
|
668
|
+
return_type,
|
669
|
+
class_method: class_method
|
670
|
+
)
|
671
|
+
end
|
672
|
+
elsif kind == :attr
|
673
|
+
case attr_direction
|
674
|
+
when :reader, :accessor, :writer
|
675
|
+
attr_type = return_type
|
676
|
+
else
|
677
|
+
raise "unknown attribute direction #{attr_direction}"
|
678
|
+
end
|
679
|
+
|
680
|
+
def_names.map do |def_name|
|
681
|
+
RbiGenerator::Attribute.new(
|
682
|
+
DetachedRbiGenerator.new,
|
683
|
+
def_name,
|
684
|
+
attr_direction,
|
685
|
+
attr_type,
|
686
|
+
class_attribute: class_method
|
687
|
+
)
|
688
|
+
end
|
689
|
+
else
|
690
|
+
raise "unknown definition kind #{kind}"
|
691
|
+
end
|
692
|
+
end
|
693
|
+
|
493
694
|
protected
|
494
695
|
|
495
696
|
sig { params(node: T.nilable(Parser::AST::Node)).returns(T::Array[Symbol]) }
|
@@ -518,6 +719,24 @@ module Parlour
|
|
518
719
|
node.to_a[0].to_a[1] == :sig
|
519
720
|
end
|
520
721
|
|
722
|
+
sig { params(path: NodePath).returns(T::Boolean) }
|
723
|
+
# Given a path, returns a boolean indicating whether the previous sibling
|
724
|
+
# represents a call to "sig" with a block.
|
725
|
+
#
|
726
|
+
# @param [NodePath] path The path to the namespace definition.
|
727
|
+
# @return [Boolean] True if that node represents a "sig" call, false
|
728
|
+
# otherwise.
|
729
|
+
def previous_sibling_sig_node?(path)
|
730
|
+
previous_sibling = path.sibling(-1)
|
731
|
+
previous_node = previous_sibling.traverse(ast)
|
732
|
+
sig_node?(previous_node)
|
733
|
+
rescue IndexError, ArgumentError, TypeError
|
734
|
+
# `sibling` call could raise IndexError or ArgumentError if reaching into negative indices
|
735
|
+
# `traverse` call could raise TypeError if path doesn't return Parser::AST::Node
|
736
|
+
|
737
|
+
false
|
738
|
+
end
|
739
|
+
|
521
740
|
sig { params(node: T.nilable(Parser::AST::Node)).returns(T.nilable(String)) }
|
522
741
|
# Given an AST node, returns the source code from which it was constructed.
|
523
742
|
# If the given AST node is nil, this returns nil.
|
data/lib/parlour/version.rb
CHANGED
data/parlour.gemspec
CHANGED
@@ -28,7 +28,7 @@ Gem::Specification.new do |spec|
|
|
28
28
|
spec.add_dependency "parser"
|
29
29
|
|
30
30
|
spec.add_development_dependency "bundler", "~> 2.0"
|
31
|
-
spec.add_development_dependency "rake", "
|
31
|
+
spec.add_development_dependency "rake", ">= 12.3.3"
|
32
32
|
spec.add_development_dependency "rspec", "~> 3.0"
|
33
33
|
spec.add_development_dependency "sorbet"
|
34
34
|
spec.add_development_dependency "simplecov"
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: parlour
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 3.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Aaron Christiansen
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2020-
|
11
|
+
date: 2020-05-15 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: sorbet-runtime
|
@@ -84,16 +84,16 @@ dependencies:
|
|
84
84
|
name: rake
|
85
85
|
requirement: !ruby/object:Gem::Requirement
|
86
86
|
requirements:
|
87
|
-
- - "
|
87
|
+
- - ">="
|
88
88
|
- !ruby/object:Gem::Version
|
89
|
-
version:
|
89
|
+
version: 12.3.3
|
90
90
|
type: :development
|
91
91
|
prerelease: false
|
92
92
|
version_requirements: !ruby/object:Gem::Requirement
|
93
93
|
requirements:
|
94
|
-
- - "
|
94
|
+
- - ">="
|
95
95
|
- !ruby/object:Gem::Version
|
96
|
-
version:
|
96
|
+
version: 12.3.3
|
97
97
|
- !ruby/object:Gem::Dependency
|
98
98
|
name: rspec
|
99
99
|
requirement: !ruby/object:Gem::Requirement
|
@@ -177,6 +177,8 @@ files:
|
|
177
177
|
- lib/parlour/rbi_generator/options.rb
|
178
178
|
- lib/parlour/rbi_generator/parameter.rb
|
179
179
|
- lib/parlour/rbi_generator/rbi_object.rb
|
180
|
+
- lib/parlour/rbi_generator/struct_class_namespace.rb
|
181
|
+
- lib/parlour/rbi_generator/struct_prop.rb
|
180
182
|
- lib/parlour/type_loader.rb
|
181
183
|
- lib/parlour/type_parser.rb
|
182
184
|
- lib/parlour/version.rb
|