parlour 3.0.0 → 4.0.0

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