parlour 3.0.0 → 4.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: 1e2b9021c22592632f30a0a51c17cfbf3838c18061638297e9437c1dd34b5c9f
4
- data.tar.gz: 337e2ea6d8212e6c2d16d2f8f5577b16bfd1c8ffa41197fad70f9dc7c51620b5
3
+ metadata.gz: f37a6dd2dffd0b59594b4c0ed664cdd27c4ea5bb02d1ca0f944ddff2d6ccda83
4
+ data.tar.gz: 38b776b79c349f48e672fbb81534d4ab1b751af13ecdab9e60d3f08562f03332
5
5
  SHA512:
6
- metadata.gz: 22e0196918f8214827f9904e45fff8689f4609ec71683ad4aca36c8eb75593e19f5d0ec4e5a5e034afdca1d3bdf7f0d9482bfb24b0e3e667553c13b5cb5b65a6
7
- data.tar.gz: 2b322d3dcdce74985b2104a8d8beb8ac0a7b6b4de1bcc8af7288d5334d715765285cba032017c7ab6e5dccde045b284dd8056e57515e13f32f319de26fdc0666
6
+ metadata.gz: 8da56118b8e784707a054ae7dbdddda63dade826ec19d87a06f468229e8e53ef1669f7b05c4c5be6cc9e75377846e729748b0f31d3400768041079050fb72776
7
+ data.tar.gz: c7a39a7a91aee7298fbdf3e5d7ebdebc278001317cb365bc7f3970d62fa49c2d9e3f5d7e1eb4c70a386ef0964f14bde2647871ab8c0b6a3a5adad8c774bc08f5
@@ -0,0 +1,5 @@
1
+ excluded_paths:
2
+ - lib/parlour/kernel_hack.rb
3
+
4
+ excluded_modules:
5
+ - Parlour::DetachedRbiGenerator
@@ -3,6 +3,16 @@ 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
+ ## [4.0.0] - 2020-05-23
7
+ ### Added
8
+ - Parlour now defaults to loading the current project when running its command
9
+ line tool, allowing it to be used as a "`sig` extractor" when run without
10
+ plugins! **Breaking if you invoke Parlour from its command line tool** - to
11
+ revert to the old behaviour of having nothing loaded into the root namespace
12
+ initially, add `parser: false` to your `.parlour` file.
13
+ - Generating constants in an eigenclass context (`class << self`) is now
14
+ supported.
15
+
6
16
  ## [3.0.0] - 2020-05-15
7
17
  ### Added
8
18
  - `T::Struct` classes can now be generated and parsed.
data/README.md CHANGED
@@ -13,7 +13,7 @@ key parts:
13
13
  RBIs for the same codebase. These are combined automatically as much as
14
14
  possible, but any other conflicts can be resolved manually through prompts.
15
15
 
16
- - The parser, which can read an RBI and convert it back into a tree of
16
+ - The parser, which can read an RBI and convert it back into a tree of
17
17
  generator objects.
18
18
 
19
19
  ## Why should I use this?
@@ -26,7 +26,10 @@ key parts:
26
26
  RBI output file.
27
27
 
28
28
  - You can **effortlessly build tools which need to access types within an RBI**;
29
- no need to write your own parser!
29
+ no need to write your own parser!
30
+
31
+ - You can **generate RBI to ship with your gem** for consuming projects to use
32
+ ([see "RBIs within gems" in Sorbet's docs](https://sorbet.org/docs/rbi#rbis-within-gems)).
30
33
 
31
34
  Please [**read the wiki**](https://github.com/AaronC81/parlour/wiki) to get
32
35
  started!
@@ -174,6 +177,29 @@ Parlour::TypeLoader.load_project('root/of/the/project')
174
177
  The structure of the returned object trees is identical to those you would
175
178
  create when generating an RBI, built of instances of `RbiObject` subclasses.
176
179
 
180
+ ## Generating RBI for a Gem
181
+
182
+ Include `parlour` as a development_dependency in your `.gemspec`:
183
+
184
+ ```ruby
185
+ spec.add_development_dependency 'parlour'
186
+ ```
187
+
188
+ Run Parlour from the command line:
189
+
190
+ ```ruby
191
+ bundle exec parlour
192
+ ```
193
+
194
+ Parlour is configured to use sane defaults assuming a standard gem structure
195
+ to generate an RBI that Sorbet will automatically find when your gem is included
196
+ as a dependency. If you require more advanced configuration you can add a
197
+ `.parlour` YAML file in the root of your project (see this project's `.parlour`
198
+ file as an example).
199
+
200
+ To disable the parsing step entire and just run plugins you can set `parser: false`
201
+ in your `.parlour` file.
202
+
177
203
  ## Parlour Plugins
178
204
 
179
205
  _Have you written an awesome Parlour plugin? Please submit a PR to add it to this list!_
@@ -16,15 +16,42 @@ command :run do |c|
16
16
  c.description = 'Generates an RBI file from your .parlour file'
17
17
 
18
18
  c.action do |args, options|
19
- configuration = keys_to_symbols(YAML.load_file(File.join(Dir.pwd, '.parlour')))
19
+ working_dir = Dir.pwd
20
+ config_filename = File.join(working_dir, '.parlour')
20
21
 
21
- raise 'you must specify output_file in your .parlour file' unless configuration[:output_file]
22
+ if File.exists?(config_filename)
23
+ configuration = keys_to_symbols(YAML.load_file(config_filename))
24
+ else
25
+ configuration = {}
26
+ end
27
+
28
+ # Output default
29
+ configuration[:output_file] ||= "rbi/#{File.basename(working_dir)}.rbi"
22
30
 
23
31
  # Style defaults
24
32
  configuration[:style] ||= {}
25
33
  configuration[:style][:tab_size] ||= 2
26
34
  configuration[:style][:break_params] ||= 4
27
35
 
36
+ # Parser defaults, set explicitly to false to not run parser
37
+ if configuration[:parser] != false
38
+ configuration[:parser] ||= {}
39
+
40
+ # Input/Output defaults
41
+ configuration[:parser][:root] ||= '.'
42
+
43
+ # Included/Excluded path defaults
44
+ configuration[:parser][:included_paths] ||= ['lib']
45
+ configuration[:parser][:excluded_paths] ||= ['sorbet', 'spec']
46
+
47
+ # Defaults can be overridden but we always want to exclude the output file
48
+ configuration[:parser][:excluded_paths] << configuration[:output_file]
49
+ end
50
+
51
+ # Included/Excluded module defaults
52
+ configuration[:included_modules] ||= []
53
+ configuration[:excluded_modules] ||= []
54
+
28
55
  # Require defaults
29
56
  configuration[:requires] ||= []
30
57
  configuration[:relative_requires] ||= []
@@ -53,6 +80,15 @@ command :run do |c|
53
80
  break_params: configuration[:style][:break_params],
54
81
  tab_size: configuration[:style][:tab_size]
55
82
  )
83
+
84
+ if configuration[:parser]
85
+ Parlour::TypeLoader.load_project(
86
+ configuration[:parser][:root],
87
+ inclusions: configuration[:parser][:included_paths],
88
+ exclusions: configuration[:parser][:excluded_paths],
89
+ generator: gen,
90
+ )
91
+ end
56
92
  Parlour::Plugin.run_plugins(plugin_instances, gen)
57
93
 
58
94
  # Run a pass of the conflict resolver
@@ -74,6 +110,14 @@ command :run do |c|
74
110
  choice == 0 ? nil : candidates[choice - 1]
75
111
  end
76
112
 
113
+ if !configuration[:included_modules].empty? || !configuration[:excluded_modules].empty?
114
+ remove_unwanted_modules(
115
+ gen.root,
116
+ included_modules: configuration[:included_modules],
117
+ excluded_modules: configuration[:excluded_modules],
118
+ )
119
+ end
120
+
77
121
  # Figure out strictness levels
78
122
  requested_strictness_levels = plugin_instances.map do |plugin|
79
123
  s = plugin.strictness&.to_s
@@ -98,6 +142,7 @@ command :run do |c|
98
142
  end
99
143
 
100
144
  # Write the final RBI
145
+ FileUtils.mkdir_p(File.dirname(configuration[:output_file]))
101
146
  File.write(configuration[:output_file], gen.rbi(strictness))
102
147
  end
103
148
  end
@@ -122,3 +167,24 @@ def keys_to_symbols(hash)
122
167
  ]
123
168
  end.to_h
124
169
  end
170
+
171
+ def remove_unwanted_modules(root, included_modules:, excluded_modules:, prefix: nil)
172
+ root.children.select! do |child|
173
+ module_name = "#{prefix}#{child.name}"
174
+
175
+ if child.respond_to?(:children)
176
+ remove_unwanted_modules(
177
+ child,
178
+ included_modules: included_modules,
179
+ excluded_modules: excluded_modules,
180
+ prefix: "#{module_name}::",
181
+ )
182
+ has_included_children = !child.children.empty?
183
+ end
184
+
185
+ included = included_modules.empty? ? true : included_modules.any? { |m| module_name.start_with?(m) }
186
+ excluded = excluded_modules.empty? ? false : excluded_modules.any? { |m| module_name.start_with?(m) }
187
+
188
+ (included || has_included_children) && !excluded
189
+ end
190
+ end
@@ -6,17 +6,12 @@ module Parlour
6
6
  def detached!
7
7
  raise "cannot call methods on a detached RBI generator"
8
8
  end
9
-
9
+
10
10
  sig { override.returns(Options) }
11
11
  def options
12
12
  detached!
13
13
  end
14
14
 
15
- sig { override.returns(Namespace) }
16
- def root
17
- detached!
18
- end
19
-
20
15
  sig { override.returns(T.nilable(Plugin)) }
21
16
  def current_plugin
22
17
  nil
@@ -50,7 +50,7 @@ module Parlour
50
50
  end
51
51
  end
52
52
 
53
- sig { params(options: Hash).void }
53
+ sig { params(options: T::Hash[T.untyped, T.untyped]).void }
54
54
  def initialize(options); end
55
55
 
56
56
  sig { abstract.params(root: RbiGenerator::Namespace).void }
@@ -8,6 +8,7 @@ module Parlour
8
8
  generator: RbiGenerator,
9
9
  name: String,
10
10
  value: String,
11
+ eigen_constant: T::Boolean,
11
12
  block: T.nilable(T.proc.params(x: Constant).void)
12
13
  ).void
13
14
  end
@@ -15,9 +16,12 @@ module Parlour
15
16
  #
16
17
  # @param name [String] The name of the constant.
17
18
  # @param value [String] The value of the constant, as a Ruby code string.
18
- def initialize(generator, name: '', value: '', &block)
19
+ # @param eigen_constant [Boolean] Whether this constant is defined on the
20
+ # eigenclass of the current namespace.
21
+ def initialize(generator, name: '', value: '', eigen_constant: false, &block)
19
22
  super(generator, name)
20
23
  @value = value
24
+ @eigen_constant = eigen_constant
21
25
  yield_self(&block) if block
22
26
  end
23
27
 
@@ -25,6 +29,10 @@ module Parlour
25
29
  sig { returns(String) }
26
30
  attr_reader :value
27
31
 
32
+ # @return [Boolean] Whether this constant is defined on the eigenclass
33
+ # of the current namespace.
34
+ attr_reader :eigen_constant
35
+
28
36
  sig { params(other: Object).returns(T::Boolean) }
29
37
  # Returns true if this instance is equal to another extend.
30
38
  #
@@ -32,7 +40,8 @@ module Parlour
32
40
  # subclass of it), this will always return false.
33
41
  # @return [Boolean]
34
42
  def ==(other)
35
- Constant === other && name == other.name && value == other.value
43
+ Constant === other && name == other.name && value == other.value \
44
+ && eigen_constant == other.eigen_constant
36
45
  end
37
46
 
38
47
  sig do
@@ -527,7 +527,7 @@ module Parlour
527
527
  returned_includables
528
528
  end
529
529
 
530
- sig { params(name: String, value: String, block: T.nilable(T.proc.params(x: Constant).void)).returns(Constant) }
530
+ sig { params(name: String, value: String, eigen_constant: T::Boolean, block: T.nilable(T.proc.params(x: Constant).void)).returns(Constant) }
531
531
  # Adds a new constant definition to this namespace.
532
532
  #
533
533
  # @example Add an +Elem+ constant to the class.
@@ -535,13 +535,16 @@ module Parlour
535
535
  #
536
536
  # @param name [String] The name of the constant.
537
537
  # @param value [String] The value of the constant, as a Ruby code string.
538
+ # @param eigen_constant [Boolean] Whether this constant is defined on the
539
+ # eigenclass of the current namespace.
538
540
  # @param block A block which the new instance yields itself to.
539
541
  # @return [RbiGenerator::Constant]
540
- def create_constant(name, value:, &block)
542
+ def create_constant(name, value:, eigen_constant: false, &block)
541
543
  new_constant = RbiGenerator::Constant.new(
542
544
  generator,
543
545
  name: name,
544
546
  value: value,
547
+ eigen_constant: eigen_constant,
545
548
  &block
546
549
  )
547
550
  move_next_comments(new_constant)
@@ -631,14 +634,19 @@ module Parlour
631
634
 
632
635
  result += [options.indented(indent_level, 'final!'), ''] if final
633
636
 
634
- if includes.any? || extends.any? || constants.any?
637
+ # Split away the eigen constants; these need to be put in a
638
+ # "class << self" block later
639
+ eigen_constants, non_eigen_constants = constants.partition(&:eigen_constant)
640
+ eigen_constants.sort_by!(&:name) if options.sort_namespaces
641
+
642
+ if includes.any? || extends.any? || non_eigen_constants.any?
635
643
  result += (options.sort_namespaces ? includes.sort_by(&:name) : includes)
636
644
  .flat_map { |x| x.generate_rbi(indent_level, options) }
637
645
  .reject { |x| x.strip == '' }
638
646
  result += (options.sort_namespaces ? extends.sort_by(&:name) : extends)
639
647
  .flat_map { |x| x.generate_rbi(indent_level, options) }
640
648
  .reject { |x| x.strip == '' }
641
- result += (options.sort_namespaces ? constants.sort_by(&:name) : constants)
649
+ result += (options.sort_namespaces ? non_eigen_constants.sort_by(&:name) : non_eigen_constants)
642
650
  .flat_map { |x| x.generate_rbi(indent_level, options) }
643
651
  .reject { |x| x.strip == '' }
644
652
  result << ""
@@ -658,14 +666,29 @@ module Parlour
658
666
  child.is_a?(Attribute) && child.class_attribute
659
667
  end
660
668
 
661
- if class_attributes.any?
662
- result << options.indented(indent_level, 'class << self')
669
+ # Handle the "class << self block"
670
+ result << options.indented(indent_level, 'class << self') \
671
+ if class_attributes.any? || eigen_constants.any?
672
+
673
+ if eigen_constants.any?
674
+ first, *rest = eigen_constants
675
+ result += T.must(first).generate_rbi(indent_level + 1, options) + T.must(rest)
676
+ .map { |obj| obj.generate_rbi(indent_level + 1, options) }
677
+ .map { |lines| [""] + lines }
678
+ .flatten
679
+ end
680
+
681
+ result << '' if eigen_constants.any? && class_attributes.any?
663
682
 
683
+ if class_attributes.any?
664
684
  first, *rest = class_attributes
665
685
  result += T.must(first).generate_rbi(indent_level + 1, options) + T.must(rest)
666
686
  .map { |obj| obj.generate_rbi(indent_level + 1, options) }
667
687
  .map { |lines| [""] + lines }
668
688
  .flatten
689
+ end
690
+
691
+ if class_attributes.any? || eigen_constants.any?
669
692
  result << options.indented(indent_level, 'end')
670
693
  result << ''
671
694
  end
@@ -42,7 +42,7 @@ module Parlour
42
42
 
43
43
  @kind = :keyword if kind == :normal && name.end_with?(':')
44
44
 
45
- @type = type
45
+ @type = type || 'T.untyped'
46
46
  @default = default
47
47
  end
48
48
 
@@ -80,10 +80,10 @@ module Parlour
80
80
  T.must(name[prefix.length..-1])
81
81
  end
82
82
 
83
- sig { returns(T.nilable(String)) }
83
+ sig { returns(String) }
84
84
  # A Sorbet string of this parameter's type, such as +"String"+ or
85
85
  # +"T.untyped"+.
86
- # @return [String, nil]
86
+ # @return [String]
87
87
  attr_reader :type
88
88
 
89
89
  sig { returns(T.nilable(String)) }
@@ -118,7 +118,7 @@ module Parlour
118
118
  #
119
119
  # @return [String]
120
120
  def to_sig_param
121
- "#{name_without_kind}: #{type || 'T.untyped'}"
121
+ "#{name_without_kind}: #{type}"
122
122
  end#
123
123
 
124
124
  # A mapping of {kind} values to the characteristic prefixes each kind has.
@@ -10,27 +10,34 @@ module Parlour
10
10
  # TODO: make this into a class which stores configuration and passes it to
11
11
  # all typeparsers
12
12
 
13
- sig { params(source: String, filename: T.nilable(String)).returns(RbiGenerator::Namespace) }
13
+ sig { params(source: String, filename: T.nilable(String), generator: T.nilable(RbiGenerator)).returns(RbiGenerator::Namespace) }
14
14
  # Converts Ruby source code into a tree of objects.
15
15
  #
16
16
  # @param [String] source The Ruby source code.
17
17
  # @param [String, nil] filename The filename to use when parsing this code.
18
18
  # This may be used in error messages, but is optional.
19
19
  # @return [RbiGenerator::Namespace] The root of the object tree.
20
- def self.load_source(source, filename = nil)
21
- TypeParser.from_source(filename || '(source)', source).parse_all
20
+ def self.load_source(source, filename = nil, generator: nil)
21
+ TypeParser.from_source(filename || '(source)', source, generator: generator).parse_all
22
22
  end
23
23
 
24
- sig { params(filename: String).returns(RbiGenerator::Namespace) }
24
+ sig { params(filename: String, generator: T.nilable(RbiGenerator)).returns(RbiGenerator::Namespace) }
25
25
  # Converts Ruby source code into a tree of objects from a file.
26
26
  #
27
27
  # @param [String] filename The name of the file to load code from.
28
28
  # @return [RbiGenerator::Namespace] The root of the object tree.
29
- def self.load_file(filename)
30
- load_source(File.read(filename), filename)
29
+ def self.load_file(filename, generator: nil)
30
+ load_source(File.read(filename), filename, generator: generator)
31
+ end
32
+
33
+ sig do
34
+ params(
35
+ root: String,
36
+ inclusions: T::Array[String],
37
+ exclusions: T::Array[String],
38
+ generator: T.nilable(RbiGenerator),
39
+ ).returns(RbiGenerator::Namespace)
31
40
  end
32
-
33
- sig { params(root: String, exclusions: T::Array[String]).returns(RbiGenerator::Namespace) }
34
41
  # Loads an entire Sorbet project using Sorbet's file table, obeying any
35
42
  # "typed: ignore" sigils, into a tree of objects.
36
43
  #
@@ -39,12 +46,15 @@ module Parlour
39
46
  #
40
47
  # @param [String] root The root of the project; where the "sorbet" directory
41
48
  # and "Gemfile" are located.
49
+ # @param [Array<String>] inclusions A list of files to include when loading
50
+ # the project, relative to the given root.
42
51
  # @param [Array<String>] exclusions A list of files to exclude when loading
43
52
  # the project, relative to the given root.
44
53
  # @return [RbiGenerator::Namespace] The root of the object tree.
45
- def self.load_project(root, exclusions: [])
54
+ def self.load_project(root, inclusions: ['.'], exclusions: [], generator: nil)
55
+ expanded_inclusions = inclusions.map { |i| File.expand_path(i, root) }
46
56
  expanded_exclusions = exclusions.map { |e| File.expand_path(e, root) }
47
-
57
+
48
58
  stdin, stdout, stderr, wait_thr = T.unsafe(Open3).popen3(
49
59
  'bundle exec srb tc -p file-table-json',
50
60
  chdir: root
@@ -63,14 +73,17 @@ module Parlour
63
73
  path = File.expand_path(rel_path, root)
64
74
 
65
75
  # Skip this file if it was excluded
66
- next if expanded_exclusions.include?(path)
76
+ next if !expanded_inclusions.any? { |i| path.start_with?(i) } \
77
+ || expanded_exclusions.any? { |e| path.start_with?(e) }
67
78
 
68
79
  # There are some entries which are URLs to stdlib
69
80
  next unless File.exist?(path)
70
81
 
71
- namespaces << load_file(path)
82
+ namespaces << load_file(path, generator: generator)
72
83
  end
73
84
 
85
+ namespaces.uniq!
86
+
74
87
  raise 'project is empty' if namespaces.empty?
75
88
 
76
89
  first_namespace, *other_namespaces = namespaces