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