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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 889d6730ffefc764887706e5dadc4441e66cdf32e4567e809f7e634f511b7dfc
4
- data.tar.gz: 9e9ab14b6618c209c8d194bf6e5557398689f0656247cac9561064dbcd541a6a
3
+ metadata.gz: 1e2b9021c22592632f30a0a51c17cfbf3838c18061638297e9437c1dd34b5c9f
4
+ data.tar.gz: 337e2ea6d8212e6c2d16d2f8f5577b16bfd1c8ffa41197fad70f9dc7c51620b5
5
5
  SHA512:
6
- metadata.gz: ea57b34b827ced92fdd1d28387217c0a5bc7027d94319cac6ffb238847a2820e9c164df86c9b2c43e60826cb1b362000c7c2bfc7f1f63b9b998959c70a3be377
7
- data.tar.gz: fb10c8c783aea69e412d1a6eb67dd736f3db377168a21f8025e28fdb76acfc94ed20f1f7b9a17dafbac39ebcc2d36846b350d04c3e59882b546143df374b9f41
6
+ metadata.gz: 22e0196918f8214827f9904e45fff8689f4609ec71683ad4aca36c8eb75593e19f5d0ec4e5a5e034afdca1d3bdf7f0d9482bfb24b0e3e667553c13b5cb5b65a6
7
+ data.tar.gz: 2b322d3dcdce74985b2104a8d8beb8ac0a7b6b4de1bcc8af7288d5334d715765285cba032017c7ab6e5dccde045b284dd8056e57515e13f32f319de26fdc0666
@@ -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
@@ -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`.
@@ -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
- grouped_by_name_children = namespace.children.group_by(&:name)
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
- first, *rest = namespaces
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[EnumClassNamespace]) rescue (return false)
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 && all.map(&:enums).uniq.length <= 1)
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
@@ -108,7 +108,9 @@ module Parlour
108
108
  buffer = Parser::Source::Buffer.new(filename)
109
109
  buffer.source = source
110
110
 
111
- TypeParser.new(Parser::CurrentRuby.new.parse(buffer))
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
- final_obj = RbiGenerator::ClassNamespace.new(
173
- DetachedRbiGenerator.new,
174
- this_name.to_s,
175
- final,
176
- node_to_s(superclass),
177
- abstract,
178
- ) do |c|
179
- c.children.concat(parse_path_to_object(path.child(2))) if body
180
- c.create_includes(includes)
181
- c.create_extends(extends)
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
- # TODO: Support for defs without sigs
238
- # If so, we need some kind of state machine to determine whether
239
- # they've already been dealt with by the "when :send" clause and
240
- # #parse_sig_into_methods.
241
- # If not, just ignore this.
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.
@@ -1,5 +1,5 @@
1
1
  # typed: strong
2
2
  module Parlour
3
3
  # The library version.
4
- VERSION = '2.1.0'
4
+ VERSION = '3.0.0'
5
5
  end
@@ -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", "~> 10.0"
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: 2.1.0
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-03-22 00:00:00.000000000 Z
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: '10.0'
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: '10.0'
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