typerb 0.4.0 → 0.5.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: 540b60be5aed7792e525d276c23d892343db1ea43db239d5c4b6df3664011d65
4
- data.tar.gz: 9176075161a22f47297e406854b58f7420832630a8792c70b8dd0c49a0049860
3
+ metadata.gz: 548c823e7f9d7185d069b2bb18f370bdcb8b18603e82de22b78712c251dc4a14
4
+ data.tar.gz: 637a184fa56802fdb93a60020e7eb633692e606726215dce533de62367edd3fe
5
5
  SHA512:
6
- metadata.gz: 68ab68e33e41b5f07790814d387bd74f6924445ee44b5faf78d4a80077466ffd8c323fdff4850e746cb2ed8a08bfc3b11c576fdd6ad29014a7f116d0b758bf2e
7
- data.tar.gz: 4bc5f3c347f2a61006e3097af2a96684a80fcc8b3471957be016466844667083092b050a2bebe564ded578365ec901ef5f11a9a1d14dfb86e21a331a2cb97c6a
6
+ metadata.gz: d1cf6e4223cae7543e133ede58d02a0016e8566501e1642afd85aa36ffea47e09ce3a0135fe81bd805461dc5dd163c546d5b65148f19bc1ebbab75694dbb05bf
7
+ data.tar.gz: '0486a293dea0eca23bdd4aa56f02a0a7fdd4c35d7d317a3c34683930f5642b903714c974a63a2b06533809703fecb549716e8e300b5d4248b1b84d4a7ce1ee3f'
data/CHANGELOG.md ADDED
@@ -0,0 +1,18 @@
1
+ # Changelog
2
+
3
+ ## 0.5.0
4
+
5
+ - Variable name detection rewritten: the whole caller file is parsed instead of a single stripped line.
6
+ Prism is used on Ruby 3.3+, `RubyVM::AbstractSyntaxTree` on 3.0-3.2. Both backends behave identically.
7
+ - Fixed `SyntaxError` escaping from `type!` and friends when the call spanned several lines
8
+ (leading-dot chains, multi-line argument lists).
9
+ - Variable name is now reported for multi-line calls, assignments, string interpolation and chained
10
+ receivers (`h.fetch(:a).type!(String)`).
11
+ - `subset_of!` raises `ArgumentError` instead of `NoMethodError` when the receiver is not `Enumerable`.
12
+ - Variable names are reported correctly on lines containing multibyte characters.
13
+ - Minimum Ruby is 3.0.
14
+ - Removed Guard from development dependencies.
15
+
16
+ ## 0.4.0
17
+
18
+ - Added `subset_of!`.
data/README.md CHANGED
@@ -1,9 +1,9 @@
1
1
  [![Gem Version](https://badge.fury.io/rb/typerb.svg)](https://badge.fury.io/rb/typerb)
2
- [![CI RSpec & Rubocop](https://github.com/olegantonyan/typerb/actions/workflows/tests.yml/badge.svg)](https://github.com/olegantonyan/typerb/actions/workflows/tests.yml)
2
+ [![CI](https://github.com/olegantonyan/typerb/actions/workflows/tests.yml/badge.svg)](https://github.com/olegantonyan/typerb/actions/workflows/tests.yml)
3
3
 
4
4
  # Typerb
5
5
 
6
- Proof of concept type-checking library for Ruby 2.6. Works with previous versions too with some limitation (see below).
6
+ Typecheck sugar for Ruby. Requires Ruby 3.0 or newer.
7
7
 
8
8
  ```ruby
9
9
  class A
@@ -22,32 +22,33 @@ class A
22
22
  end
23
23
 
24
24
  def call_with_subset(arg)
25
- arg.subset_of!([:one, :two])
25
+ arg.subset_of!(%i[one two])
26
26
  end
27
27
  end
28
28
 
29
- A.new.call(1) #=> TypeError: `some_arg` should be String or Symbol, not Integer
30
- A.new.call_with_respond_checks(1) #=> TypeError: 'Integer should respond to all methods: strip'
31
- A.new.call_with_enum(:three) #=> TypeError: 'Symbol (`arg`) should be one of: [one, two], not three'
32
- A.new.call_with_subset([:one, :three]) #=> TypeError: 'Array (`arg`) should be subset of: [:one, :two], not [:one, :three]'
29
+ A.new.call(1) #=> TypeError: `some_arg` should be String or Symbol, not Integer (1)
30
+ A.new.call_with_respond_checks(1) #=> TypeError: Integer (`some_arg`) should respond to all methods: strip
31
+ A.new.call_with_enum(:three) #=> TypeError: Symbol (`arg`) should be one of: [one, two], not three
32
+ A.new.call_with_subset(%i[one three]) #=> TypeError: Array (`arg`) should be subset of: [:one, :two], not [:one, :three]
33
33
  ```
34
34
 
35
35
  This is equivalent to:
36
+
36
37
  ```ruby
37
38
  class A
38
39
  def call(some_arg)
39
- raise TypeError, "`some_arg` should be String or Symbol, not #{some_arg.class}" unless [String, Symbol].include?(some_arg.class)
40
+ raise TypeError, "`some_arg` should be String or Symbol, not #{some_arg.class}" unless some_arg.is_a?(String) || some_arg.is_a?(Symbol)
40
41
  end
41
42
 
42
43
  def call_with_respond_checks(some_arg)
43
- raise TypeError, "#{some_arg.class} should respond to all methods: strip" unless [:strip].all{|meth| some_arg.respond_to?(meth)}
44
+ raise TypeError, "#{some_arg.class} should respond to all methods: strip" unless %i[strip].all? { |meth| some_arg.respond_to?(meth) }
44
45
  end
45
46
  end
46
47
  ```
47
48
 
48
- But without boilerplate.
49
+ But without the boilerplate.
49
50
 
50
- It also has `not_nil!` method, similar to Crystal language.
51
+ There is also a `not_nil!` method, similar to the Crystal language.
51
52
 
52
53
  ```ruby
53
54
  class A
@@ -58,13 +59,15 @@ class A
58
59
  end
59
60
  end
60
61
 
61
- A.new.call(nil) #=> TypeError: expected not nil, but got nil
62
+ A.new.call(nil) #=> TypeError: `some_arg` should not be nil
62
63
  ```
63
64
 
65
+ Every method returns `self` when the check passes, so checks can be chained or inlined into assignments.
66
+
64
67
  ## Why?
65
68
 
66
- 1. Catch error as early as possible (especially nils);
67
- 2. Additional documentation: you're telling other people more about interfaces.
69
+ 1. Catch errors as early as possible (especially nils);
70
+ 2. Additional documentation: you're telling other people more about your interfaces.
68
71
 
69
72
  ## Installation
70
73
 
@@ -82,20 +85,9 @@ Or install it yourself as:
82
85
 
83
86
  $ gem install typerb
84
87
 
85
- If this fails with error
86
- ```
87
- ERROR: Error installing typerb:
88
- There are no versions of typerb (>= 0) compatible with your Ruby & RubyGems
89
- typerb requires Ruby version >= 2.6.0.pre.preview3. The current ruby version is 2.6.0.
90
- ```
91
- even when you have Ruby 2.6.0-preview3 installed, then try installing it through Gemfile from git:
92
- ```ruby
93
- gem 'typerb', github: 'olegantonyan/typerb'
94
- ```
95
-
96
88
  ## Usage
97
89
 
98
- 1. Add `using Typerb` to a class where you want to have type check.
90
+ 1. Add `using Typerb` to a class where you want to have type checks.
99
91
  2. Call `.type!()` on any object to assert its type.
100
92
  3. PROFIT! No more "NoMethodError for nil" 10 methods up the stack. You'll know exactly where this nil came from.
101
93
 
@@ -112,33 +104,44 @@ class A
112
104
  end
113
105
  ```
114
106
 
115
- If you're unfamiliar with `using` keyword - this is refinement - a relatively new feature in Ruby (since 2.0). It's kind of monkey-patch, but with strict scope. Learn more about [refinements](https://ruby-doc.org/core-2.5.3/doc/syntax/refinements_rdoc.html).
107
+ If you're unfamiliar with the `using` keyword - this is a refinement, a kind of monkey patch with a strict
108
+ scope. Learn more about [refinements](https://docs.ruby-lang.org/en/master/syntax/refinements_rdoc.html).
109
+
110
+ The refinement adds `type!`, `not_nil!`, `respond_to!`, `enum!` and `subset_of!` to `BasicObject`, so
111
+ they can be called on any object.
112
+
113
+ `type!` raises a `TypeError` unless `self` is an instance of one of the classes passed as arguments.
114
+ The tricky part is getting the name of the variable it was called on, so that the error message points at
115
+ the exact variable instead of being an abstract `TypeError`. Typerb does that by parsing the source file
116
+ of the caller: with [Prism](https://github.com/ruby/prism) on Ruby 3.3+, and with `RubyVM::AbstractSyntaxTree`
117
+ on older versions. If neither is available, or the source cannot be read, the check still works - the message
118
+ just doesn't name the variable.
116
119
 
117
- This refinement adds `type!()` and `not_nil!` methods to `BasicObject` class so you can call it on any object.
120
+ | Ruby | Parser |
121
+ | --------- | ---------------------------- |
122
+ | 3.3+ | Prism |
123
+ | 3.0 - 3.2 | `RubyVM::AbstractSyntaxTree` |
118
124
 
119
- The method will raise an exception if `self` is not an instance of one of the classes passed as arguments. The tricky part, however, is to get the variable name on which it's called. You need this to get a nice error message telling you exactly which variable has wrong type, not just an abstract `TypeError`. That's why we need Ruby 2.6 with its new `RubyVM::AST` (https://ruby-doc.org/core-2.6.0.preview3/RubyVM/AST.html).
125
+ Both parsers produce the same messages, and CI runs the suite against every supported version.
120
126
 
121
127
  ## Limitations
122
128
 
123
- Full functionality Ruby 2.6.0-preview3. Relies on `RubyVM::AST` which may change in release version. So, expect breaking changes in Ruby. Previous versions also supported, but without variable name in exception message.
129
+ The variable name is omitted (the check itself still works) in two cases.
124
130
 
125
- Known limitations:
131
+ 1. Several checks on the same line - there is no way to tell which one raised:
126
132
 
127
- 1. Multi-line method call:
128
133
  ```ruby
129
134
  class A
130
135
  using Typerb
131
136
 
132
- def call(some_arg)
133
- some_arg.
134
- type!(String)
135
- # this won't work. type!() call must be on the same line with the variable it's called on - raise error message without variable name
136
- # some_arg. type!(String) is ok though
137
+ def initialize(arg1, arg2)
138
+ arg1.type!(Integer); arg2.type!(String)
137
139
  end
138
140
  end
139
141
  ```
140
142
 
141
- 2. Method defined in console:
143
+ 2. Code whose source file cannot be read - `eval`, a console session, or a file deleted after being loaded:
144
+
142
145
  ```ruby
143
146
  [1] pry(main)> class A
144
147
  [1] pry(main)* using Typerb
@@ -147,34 +150,26 @@ end
147
150
  [1] pry(main)* end
148
151
  [1] pry(main)* end
149
152
  [2] pry(main)> A.new.call(1)
150
- TypeError: expected Hash, got Integer
151
- # here we cannot get the source code for a line containing "a.type!(Hash)", so cannot see the variable name
152
- ```
153
-
154
- 3. Multiple arguments on the same line:
155
- ```ruby
156
- class A
157
- using Typerb
158
-
159
- def initialize(arg1, arg2)
160
- arg1.type!(Integer); arg2.type!(String)
161
- # no way to tell the variable - raise error message without variable name
162
- # same error will be raised on Ruby < 2.6.0 because there is no RubyVM::AST
163
- end
164
- end
153
+ TypeError: expected Hash, got Integer (1)
165
154
  ```
166
155
 
167
- These limitations shouldn't be a problem in any case. Please, file an issue if you know a scenario where one of these could be a real problem.
156
+ Please file an issue if you know a scenario where one of these is a real problem.
168
157
 
169
158
  ## Development
170
159
 
171
- After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
160
+ After checking out the repo, run `bin/setup` to install dependencies. Then run `rake test` to run the tests.
161
+ You can also run `bin/console` for an interactive prompt that will allow you to experiment.
172
162
 
173
- To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
163
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version,
164
+ update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git
165
+ tag for the version, push git commits and tags, and push the `.gem` file to
166
+ [rubygems.org](https://rubygems.org).
174
167
 
175
168
  ## Contributing
176
169
 
177
- Bug reports and pull requests are welcome on GitHub at https://github.com/olegantonyan/typerb. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
170
+ Bug reports and pull requests are welcome on GitHub at https://github.com/olegantonyan/typerb. This project
171
+ is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the
172
+ [Contributor Covenant](http://contributor-covenant.org) code of conduct.
178
173
 
179
174
  ## License
180
175
 
@@ -182,4 +177,5 @@ The gem is available as open source under the terms of the [MIT License](https:/
182
177
 
183
178
  ## Code of Conduct
184
179
 
185
- Everyone interacting in the Typerb projects codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/olegantonyan/typerb/blob/master/CODE_OF_CONDUCT.md).
180
+ Everyone interacting in the Typerb project's codebases, issue trackers, chat rooms and mailing lists is
181
+ expected to follow the [code of conduct](https://github.com/olegantonyan/typerb/blob/master/CODE_OF_CONDUCT.md).
@@ -1,27 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Typerb
4
- class Exceptional # NOTE: don't want to collide with 'Exception' class name
5
- class << self
6
- def klasses_text(klasses)
7
- klasses.size > 1 ? klasses.map(&:name).join(' or ') : klasses.first.name
8
- end
9
-
10
- def methods_text(methods)
11
- methods.join(', ')
12
- end
13
-
14
- def elements_text(elements)
15
- '[' + elements.join(', ') + ']'
16
- end
3
+ require 'typerb/variable_name'
17
4
 
18
- def superset_text(enumerable)
19
- enumerable.to_s
20
- end
21
- end
22
-
23
- def raise_with(backtrace, exception_text)
24
- exception = TypeError.new(exception_text)
5
+ module Typerb
6
+ module Exceptional # NOTE: don't want to collide with 'Exception' class name
7
+ def self.raise_type_error(backtrace, location, method_name)
8
+ exception = TypeError.new(yield(VariableName.new(location, method_name).get))
25
9
  exception.set_backtrace(backtrace)
26
10
  raise exception
27
11
  end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ begin
4
+ require 'prism'
5
+ rescue LoadError # rubocop: disable Lint/SuppressedException
6
+ end
7
+
8
+ require 'typerb/source_cache'
9
+
10
+ module Typerb
11
+ module PrismParser
12
+ RECEIVER_NODE_NAMES = %w[
13
+ LocalVariableReadNode
14
+ InstanceVariableReadNode
15
+ ClassVariableReadNode
16
+ GlobalVariableReadNode
17
+ ConstantReadNode
18
+ ConstantPathNode
19
+ CallNode
20
+ ].freeze
21
+
22
+ class << self
23
+ def available?
24
+ defined?(::Prism) && ::Prism.respond_to?(:parse_file)
25
+ end
26
+
27
+ def receiver_sources(file, line, method_name)
28
+ calls(root(file), line, method_name).map { |node| receiver_source(node) }
29
+ end
30
+
31
+ private
32
+
33
+ def root(file)
34
+ SourceCache.read(file, :prism) { ::Prism.parse_file(file).value }
35
+ end
36
+
37
+ def calls(root, line, method_name)
38
+ found = []
39
+ stack = [root]
40
+ until stack.empty?
41
+ node = stack.pop
42
+ found << node if call?(node, line, method_name)
43
+ stack.concat(node.compact_child_nodes)
44
+ end
45
+ found
46
+ end
47
+
48
+ def call?(node, line, method_name)
49
+ node.is_a?(::Prism::CallNode) &&
50
+ node.name.to_sym == method_name &&
51
+ node.message_loc&.start_line == line
52
+ end
53
+
54
+ def receiver_source(node)
55
+ receiver = node.receiver
56
+ return nil unless receiver_nodes.any? { |kls| receiver.is_a?(kls) }
57
+
58
+ receiver.slice
59
+ end
60
+
61
+ def receiver_nodes
62
+ @receiver_nodes ||= RECEIVER_NODE_NAMES.select { |name| ::Prism.const_defined?(name) }
63
+ .map { |name| ::Prism.const_get(name) }
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'typerb/source_cache'
4
+
5
+ module Typerb
6
+ module RubyVmParser
7
+ CALL_TYPES = %i[CALL QCALL].freeze
8
+ RECEIVER_TYPES = %i[LVAR DVAR IVAR CVAR GVAR CONST COLON2 COLON3 CALL QCALL VCALL FCALL ITER].freeze
9
+
10
+ class << self
11
+ def available?
12
+ defined?(::RubyVM::AbstractSyntaxTree) && ::RubyVM::AbstractSyntaxTree.respond_to?(:parse_file)
13
+ end
14
+
15
+ def receiver_sources(file, line, method_name)
16
+ calls(root(file), line, method_name).map { |node| receiver_source(file, node) }
17
+ end
18
+
19
+ private
20
+
21
+ def root(file)
22
+ SourceCache.read(file, :ruby_vm) { ::RubyVM::AbstractSyntaxTree.parse_file(file) }
23
+ end
24
+
25
+ def calls(root, line, method_name)
26
+ found = []
27
+ stack = [root]
28
+ until stack.empty?
29
+ node = stack.pop
30
+ found << node if call?(node, line, method_name)
31
+ stack.concat(node.children.grep(::RubyVM::AbstractSyntaxTree::Node))
32
+ end
33
+ found
34
+ end
35
+
36
+ def call?(node, line, method_name)
37
+ CALL_TYPES.include?(node.type) &&
38
+ node.children[1] == method_name &&
39
+ (node.first_lineno..node.last_lineno).cover?(line)
40
+ end
41
+
42
+ def receiver_source(file, node)
43
+ receiver = node.children.first
44
+ return nil unless receiver.is_a?(::RubyVM::AbstractSyntaxTree::Node)
45
+ return nil unless RECEIVER_TYPES.include?(receiver.type)
46
+ return nil unless receiver.first_lineno == receiver.last_lineno
47
+
48
+ line = lines(file)[receiver.first_lineno - 1]
49
+ line&.byteslice(receiver.first_column...receiver.last_column) # AST columns are byte offsets
50
+ end
51
+
52
+ def lines(file)
53
+ SourceCache.read(file, :lines) { File.readlines(file) }
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Typerb
4
+ module SourceCache
5
+ LIMIT = 32
6
+
7
+ class << self
8
+ def read(file, kind)
9
+ stat = File.stat(file)
10
+ key = [file, kind, stat.mtime, stat.size]
11
+ mutex.synchronize do
12
+ return store[key] if store.key?(key)
13
+
14
+ store.shift while store.size >= LIMIT
15
+ store[key] = yield
16
+ end
17
+ end
18
+
19
+ private
20
+
21
+ def store
22
+ @store ||= {}
23
+ end
24
+
25
+ def mutex
26
+ @mutex ||= Mutex.new
27
+ end
28
+ end
29
+ end
30
+ end
@@ -1,30 +1,34 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'typerb/prism_parser'
4
+ require 'typerb/ruby_vm_parser'
5
+
3
6
  module Typerb
4
7
  class VariableName
5
- attr_reader :file, :line
8
+ BACKENDS = [PrismParser, RubyVmParser].freeze
9
+
10
+ attr_reader :file, :line, :method_name
6
11
 
7
- def initialize(caller_loc)
8
- @file = caller_loc[0].path
9
- @line = caller_loc[0].lineno
12
+ def initialize(location, method_name)
13
+ @file = location&.path
14
+ @line = location&.lineno
15
+ @method_name = method_name
10
16
  end
11
17
 
12
18
  def get
13
- return unless defined?(RubyVM::AbstractSyntaxTree)
14
- return unless File.exist?(file)
19
+ return unless backend
20
+ return unless file && line && File.exist?(file)
15
21
 
16
- caller_method = caller_locations(1, 1)[0].label.to_sym
17
- from_ast(caller_method)
22
+ receivers = backend.receiver_sources(file, line, method_name)
23
+ receivers.first if receivers.size == 1
24
+ rescue StandardError, ScriptError
25
+ nil
18
26
  end
19
27
 
20
28
  private
21
29
 
22
- def from_ast(caller_method) # rubocop: disable Metrics/AbcSize not worth fixing
23
- code = File.read(file).lines[line - 1].strip
24
- node = RubyVM::AbstractSyntaxTree.parse(code)
25
- if node.children.last.children.size == 3 && node.children.last.children[1] == caller_method # rubocop: disable Style/IfUnlessModifier, Style/GuardClause
26
- node.children.last.children.first.children.first
27
- end
30
+ def backend
31
+ BACKENDS.find(&:available?)
28
32
  end
29
33
  end
30
34
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Typerb
4
- VERSION = '0.4.0'
4
+ VERSION = '0.5.0'
5
5
  end
data/lib/typerb.rb CHANGED
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'typerb/version'
4
- require 'typerb/variable_name'
5
4
  require 'typerb/exceptional'
6
5
 
7
6
  module Typerb
@@ -10,69 +9,51 @@ module Typerb
10
9
  raise ArgumentError, 'provide at least one class' if klasses.empty?
11
10
  return self if klasses.any? { |kls| is_a?(kls) }
12
11
 
13
- klasses_text = Typerb::Exceptional.klasses_text(klasses)
14
- exception_text = if (var_name = Typerb::VariableName.new(caller_locations(1, 1)).get)
15
- "`#{var_name}` should be #{klasses_text}, not #{self.class} (#{self})"
16
- else
17
- "expected #{klasses_text}, got #{self.class} (#{self})"
18
- end
19
-
20
- Typerb::Exceptional.new.raise_with(caller, exception_text)
12
+ klasses_text = klasses.join(' or ')
13
+ Typerb::Exceptional.raise_type_error(caller, caller_locations(1, 1)[0], :type!) do |var_name|
14
+ var_name ? "`#{var_name}` should be #{klasses_text}, not #{self.class} (#{self})" : "expected #{klasses_text}, got #{self.class} (#{self})"
15
+ end
21
16
  end
22
17
 
23
18
  def not_nil!
24
- return self unless self.nil? # rubocop: disable Style/RedundantSelf rubocop breaks without reundant self
25
-
26
- exception_text = if (var_name = Typerb::VariableName.new(caller_locations(1, 1)).get)
27
- "`#{var_name}` should not be nil"
28
- else
29
- 'expected not nil, got nil'
30
- end
19
+ return self unless self.nil? # rubocop: disable Style/RedundantSelf
31
20
 
32
- Typerb::Exceptional.new.raise_with(caller, exception_text)
21
+ Typerb::Exceptional.raise_type_error(caller, caller_locations(1, 1)[0], :not_nil!) do |var_name|
22
+ var_name ? "`#{var_name}` should not be nil" : 'expected not nil, got nil'
23
+ end
33
24
  end
34
25
 
35
26
  def respond_to!(*methods)
36
27
  raise ArgumentError, 'provide at least one method' if methods.empty?
37
28
  return self if methods.all? { |meth| respond_to?(meth) }
38
29
 
39
- methods_text = Typerb::Exceptional.methods_text(methods)
40
- exception_text = if (var_name = Typerb::VariableName.new(caller_locations(1, 1)).get)
41
- "#{self.class} (`#{var_name}`) should respond to all methods: #{methods_text}"
42
- else
43
- "#{self.class} should respond to all methods: #{methods_text}"
44
- end
45
-
46
- Typerb::Exceptional.new.raise_with(caller, exception_text)
30
+ methods_text = methods.join(', ')
31
+ Typerb::Exceptional.raise_type_error(caller, caller_locations(1, 1)[0], :respond_to!) do |var_name|
32
+ var_name ? "#{self.class} (`#{var_name}`) should respond to all methods: #{methods_text}" : "#{self.class} should respond to all methods: #{methods_text}"
33
+ end
47
34
  end
48
35
 
49
36
  def enum!(*elements)
50
37
  raise ArgumentError, 'provide at least one enum element' if elements.empty?
51
38
  return self if elements.include?(self)
52
39
 
53
- elements_text = Typerb::Exceptional.elements_text(elements)
54
- exception_text = if (var_name = Typerb::VariableName.new(caller_locations(1, 1)).get)
55
- "#{self.class} (`#{var_name}`) should be one of: #{elements_text}, not #{self}"
56
- else
57
- "#{self.class} expected one of: #{elements_text}, got #{self}"
58
- end
59
-
60
- Typerb::Exceptional.new.raise_with(caller, exception_text)
40
+ elements_text = "[#{elements.join(', ')}]"
41
+ Typerb::Exceptional.raise_type_error(caller, caller_locations(1, 1)[0], :enum!) do |var_name|
42
+ var_name ? "#{self.class} (`#{var_name}`) should be one of: #{elements_text}, not #{self}" : "#{self.class} expected one of: #{elements_text}, got #{self}"
43
+ end
61
44
  end
62
45
 
63
46
  def subset_of!(superset)
47
+ raise ArgumentError, 'receiver must be Enumerable' unless is_a?(Enumerable)
64
48
  raise ArgumentError, 'superset must be Enumerable' unless superset.is_a?(Enumerable)
65
- raise ArgumentError, 'provide at least one superset element' if superset.empty?
66
- return self if (self - superset).empty?
67
49
 
68
- superset_text = Typerb::Exceptional.superset_text(superset)
69
- exception_text = if (var_name = Typerb::VariableName.new(caller_locations(1, 1)).get)
70
- "#{self.class} (`#{var_name}`) should be subset of: #{superset_text}, not #{self}"
71
- else
72
- "#{self.class} expected subset of: #{superset_text}, got #{self}"
73
- end
50
+ elements = superset.to_a
51
+ raise ArgumentError, 'provide at least one superset element' if elements.empty?
52
+ return self if (to_a - elements).empty?
74
53
 
75
- Typerb::Exceptional.new.raise_with(caller, exception_text)
54
+ Typerb::Exceptional.raise_type_error(caller, caller_locations(1, 1)[0], :subset_of!) do |var_name|
55
+ var_name ? "#{self.class} (`#{var_name}`) should be subset of: #{superset}, not #{self}" : "#{self.class} expected subset of: #{superset}, got #{self}"
56
+ end
76
57
  end
77
58
  end
78
59
  end
metadata CHANGED
@@ -1,59 +1,16 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: typerb
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Oleg Antonyan
8
- autorequire:
9
- bindir: exe
8
+ bindir: bin
10
9
  cert_chain: []
11
- date: 2022-01-17 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
- name: bundler
15
- requirement: !ruby/object:Gem::Requirement
16
- requirements:
17
- - - ">="
18
- - !ruby/object:Gem::Version
19
- version: '1.17'
20
- type: :development
21
- prerelease: false
22
- version_requirements: !ruby/object:Gem::Requirement
23
- requirements:
24
- - - ">="
25
- - !ruby/object:Gem::Version
26
- version: '1.17'
27
- - !ruby/object:Gem::Dependency
28
- name: guard
29
- requirement: !ruby/object:Gem::Requirement
30
- requirements:
31
- - - ">="
32
- - !ruby/object:Gem::Version
33
- version: '0'
34
- type: :development
35
- prerelease: false
36
- version_requirements: !ruby/object:Gem::Requirement
37
- requirements:
38
- - - ">="
39
- - !ruby/object:Gem::Version
40
- version: '0'
41
- - !ruby/object:Gem::Dependency
42
- name: guard-rspec
43
- requirement: !ruby/object:Gem::Requirement
44
- requirements:
45
- - - ">="
46
- - !ruby/object:Gem::Version
47
- version: '0'
48
- type: :development
49
- prerelease: false
50
- version_requirements: !ruby/object:Gem::Requirement
51
- requirements:
52
- - - ">="
53
- - !ruby/object:Gem::Version
54
- version: '0'
55
- - !ruby/object:Gem::Dependency
56
- name: pry
13
+ name: minitest
57
14
  requirement: !ruby/object:Gem::Requirement
58
15
  requirements:
59
16
  - - ">="
@@ -68,34 +25,6 @@ dependencies:
68
25
  version: '0'
69
26
  - !ruby/object:Gem::Dependency
70
27
  name: rake
71
- requirement: !ruby/object:Gem::Requirement
72
- requirements:
73
- - - ">="
74
- - !ruby/object:Gem::Version
75
- version: '10.0'
76
- type: :development
77
- prerelease: false
78
- version_requirements: !ruby/object:Gem::Requirement
79
- requirements:
80
- - - ">="
81
- - !ruby/object:Gem::Version
82
- version: '10.0'
83
- - !ruby/object:Gem::Dependency
84
- name: rspec
85
- requirement: !ruby/object:Gem::Requirement
86
- requirements:
87
- - - ">="
88
- - !ruby/object:Gem::Version
89
- version: '3.0'
90
- type: :development
91
- prerelease: false
92
- version_requirements: !ruby/object:Gem::Requirement
93
- requirements:
94
- - - ">="
95
- - !ruby/object:Gem::Version
96
- version: '3.0'
97
- - !ruby/object:Gem::Dependency
98
- name: rubocop
99
28
  requirement: !ruby/object:Gem::Requirement
100
29
  requirements:
101
30
  - - ">="
@@ -109,7 +38,7 @@ dependencies:
109
38
  - !ruby/object:Gem::Version
110
39
  version: '0'
111
40
  - !ruby/object:Gem::Dependency
112
- name: super_awesome_print
41
+ name: rubocop
113
42
  requirement: !ruby/object:Gem::Requirement
114
43
  requirements:
115
44
  - - ">="
@@ -122,36 +51,33 @@ dependencies:
122
51
  - - ">="
123
52
  - !ruby/object:Gem::Version
124
53
  version: '0'
125
- description: Typecheck sugar for Ruby.
54
+ description: Refinement adding type!, not_nil!, respond_to!, enum! and subset_of!
55
+ assertions that name the variable they failed on.
126
56
  email:
127
57
  - oleg.b.antonyan@gmail.com
128
58
  executables: []
129
59
  extensions: []
130
60
  extra_rdoc_files: []
131
61
  files:
132
- - ".github/workflows/tests.yml"
133
- - ".gitignore"
134
- - ".rspec"
135
- - ".rubocop.yml"
62
+ - CHANGELOG.md
136
63
  - CODE_OF_CONDUCT.md
137
- - Gemfile
138
- - Gemfile.lock
139
- - Guardfile
140
64
  - LICENSE.txt
141
65
  - README.md
142
- - Rakefile
143
- - bin/console
144
- - bin/setup
145
66
  - lib/typerb.rb
146
67
  - lib/typerb/exceptional.rb
68
+ - lib/typerb/prism_parser.rb
69
+ - lib/typerb/ruby_vm_parser.rb
70
+ - lib/typerb/source_cache.rb
147
71
  - lib/typerb/variable_name.rb
148
72
  - lib/typerb/version.rb
149
- - typerb.gemspec
150
73
  homepage: https://github.com/olegantonyan/typerb
151
74
  licenses:
152
75
  - MIT
153
- metadata: {}
154
- post_install_message:
76
+ metadata:
77
+ source_code_uri: https://github.com/olegantonyan/typerb
78
+ changelog_uri: https://github.com/olegantonyan/typerb/blob/master/CHANGELOG.md
79
+ bug_tracker_uri: https://github.com/olegantonyan/typerb/issues
80
+ rubygems_mfa_required: 'true'
155
81
  rdoc_options: []
156
82
  require_paths:
157
83
  - lib
@@ -159,15 +85,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
159
85
  requirements:
160
86
  - - ">="
161
87
  - !ruby/object:Gem::Version
162
- version: '2.4'
88
+ version: '3.0'
163
89
  required_rubygems_version: !ruby/object:Gem::Requirement
164
90
  requirements:
165
91
  - - ">="
166
92
  - !ruby/object:Gem::Version
167
93
  version: '0'
168
94
  requirements: []
169
- rubygems_version: 3.3.3
170
- signing_key:
95
+ rubygems_version: 4.0.16
171
96
  specification_version: 4
172
97
  summary: Typecheck sugar for Ruby.
173
98
  test_files: []
@@ -1,24 +0,0 @@
1
- name: CI RSpec & Rubocop
2
-
3
- on: [push, pull_request]
4
-
5
- jobs:
6
- build:
7
- runs-on: ubuntu-latest
8
-
9
- strategy:
10
- matrix:
11
- ruby-version: [2.5.3, 2.6.0, 2.7.4, 3.0.1, 3.1.0]
12
-
13
- steps:
14
- - uses: actions/checkout@v2
15
- - name: Set up Ruby ${{ matrix.ruby-version }}
16
- uses: ruby/setup-ruby@v1
17
- with:
18
- ruby-version: ${{ matrix.ruby-version }}
19
- - name: Install dependencies
20
- run: bundle install
21
- - name: Run Rubocop
22
- run: bundle exec rubocop -DP
23
- - name: Run tests
24
- run: bundle exec rspec
data/.gitignore DELETED
@@ -1,15 +0,0 @@
1
- /.bundle/
2
- /.yardoc
3
- /_yardoc/
4
- /coverage/
5
- /doc/
6
- /pkg/
7
- /spec/reports/
8
- /tmp/
9
- /vendor/
10
- *.gem
11
-
12
- # rspec failure tracking
13
- .rspec_status
14
-
15
- .ruby-version
data/.rspec DELETED
@@ -1,3 +0,0 @@
1
- --format documentation
2
- --color
3
- --require spec_helper
data/.rubocop.yml DELETED
@@ -1,35 +0,0 @@
1
- AllCops:
2
- TargetRubyVersion: 3.1
3
- NewCops: enable
4
- SuggestExtensions: false
5
- Exclude:
6
- - 'bin/**/*'
7
- - 'Guardfile'
8
-
9
- inherit_mode:
10
- merge:
11
- - Exclude
12
-
13
- Style/CommentedKeyword:
14
- Enabled: false
15
-
16
- Style/Documentation:
17
- Enabled: false
18
-
19
- Style/Semicolon:
20
- Enabled: false
21
-
22
- Style/StringConcatenation:
23
- Enabled: false
24
-
25
- Layout/SpaceAroundMethodCallOperator:
26
- Enabled: false
27
-
28
- Gemspec/RequiredRubyVersion:
29
- Enabled: false
30
-
31
- Layout/LineLength:
32
- Max: 170
33
-
34
- Gemspec/RequireMFA:
35
- Enabled: false
data/Gemfile DELETED
@@ -1,8 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- source 'https://rubygems.org'
4
-
5
- git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }
6
-
7
- # Specify your gem's dependencies in typerb.gemspec
8
- gemspec
data/Gemfile.lock DELETED
@@ -1,97 +0,0 @@
1
- PATH
2
- remote: .
3
- specs:
4
- typerb (0.4.0)
5
-
6
- GEM
7
- remote: https://rubygems.org/
8
- specs:
9
- ast (2.4.2)
10
- awesome_print (1.9.2)
11
- coderay (1.1.3)
12
- diff-lcs (1.5.0)
13
- ffi (1.15.5)
14
- formatador (0.3.0)
15
- guard (2.18.0)
16
- formatador (>= 0.2.4)
17
- listen (>= 2.7, < 4.0)
18
- lumberjack (>= 1.0.12, < 2.0)
19
- nenv (~> 0.1)
20
- notiffany (~> 0.0)
21
- pry (>= 0.13.0)
22
- shellany (~> 0.0)
23
- thor (>= 0.18.1)
24
- guard-compat (1.2.1)
25
- guard-rspec (4.7.3)
26
- guard (~> 2.1)
27
- guard-compat (~> 1.1)
28
- rspec (>= 2.99.0, < 4.0)
29
- listen (3.7.1)
30
- rb-fsevent (~> 0.10, >= 0.10.3)
31
- rb-inotify (~> 0.9, >= 0.9.10)
32
- lumberjack (1.2.8)
33
- method_source (1.0.0)
34
- nenv (0.3.0)
35
- notiffany (0.1.3)
36
- nenv (~> 0.1)
37
- shellany (~> 0.0)
38
- parallel (1.21.0)
39
- parser (3.1.0.0)
40
- ast (~> 2.4.1)
41
- pry (0.14.1)
42
- coderay (~> 1.1)
43
- method_source (~> 1.0)
44
- rainbow (3.1.1)
45
- rake (13.0.6)
46
- rb-fsevent (0.11.0)
47
- rb-inotify (0.10.1)
48
- ffi (~> 1.0)
49
- regexp_parser (2.2.0)
50
- rexml (3.2.5)
51
- rspec (3.10.0)
52
- rspec-core (~> 3.10.0)
53
- rspec-expectations (~> 3.10.0)
54
- rspec-mocks (~> 3.10.0)
55
- rspec-core (3.10.1)
56
- rspec-support (~> 3.10.0)
57
- rspec-expectations (3.10.2)
58
- diff-lcs (>= 1.2.0, < 2.0)
59
- rspec-support (~> 3.10.0)
60
- rspec-mocks (3.10.2)
61
- diff-lcs (>= 1.2.0, < 2.0)
62
- rspec-support (~> 3.10.0)
63
- rspec-support (3.10.3)
64
- rubocop (1.24.1)
65
- parallel (~> 1.10)
66
- parser (>= 3.0.0.0)
67
- rainbow (>= 2.2.2, < 4.0)
68
- regexp_parser (>= 1.8, < 3.0)
69
- rexml
70
- rubocop-ast (>= 1.15.1, < 2.0)
71
- ruby-progressbar (~> 1.7)
72
- unicode-display_width (>= 1.4.0, < 3.0)
73
- rubocop-ast (1.15.1)
74
- parser (>= 3.0.1.1)
75
- ruby-progressbar (1.11.0)
76
- shellany (0.0.1)
77
- super_awesome_print (0.2.5)
78
- awesome_print
79
- thor (1.2.1)
80
- unicode-display_width (2.1.0)
81
-
82
- PLATFORMS
83
- ruby
84
-
85
- DEPENDENCIES
86
- bundler (>= 1.17)
87
- guard
88
- guard-rspec
89
- pry
90
- rake (>= 10.0)
91
- rspec (>= 3.0)
92
- rubocop
93
- super_awesome_print
94
- typerb!
95
-
96
- BUNDLED WITH
97
- 2.3.5
data/Guardfile DELETED
@@ -1,36 +0,0 @@
1
- # A sample Guardfile
2
- # More info at https://github.com/guard/guard#readme
3
-
4
- ## Uncomment and set this to only include directories you want to watch
5
- # directories %w(app lib config test spec features) \
6
- # .select{|d| Dir.exists?(d) ? d : UI.warning("Directory #{d} does not exist")}
7
-
8
- ## Note: if you are using the `directories` clause above and you are not
9
- ## watching the project directory ('.'), then you will want to move
10
- ## the Guardfile to a watched dir and symlink it back, e.g.
11
- #
12
- # $ mkdir config
13
- # $ mv Guardfile config/
14
- # $ ln -s config/Guardfile .
15
- #
16
- # and, you'll have to watch "config/Guardfile" instead of "Guardfile"
17
- guard :rspec, cmd: "bundle exec rspec" do
18
- require "guard/rspec/dsl"
19
- dsl = Guard::RSpec::Dsl.new(self)
20
-
21
- # Feel free to open issues for suggestions and improvements
22
-
23
- # RSpec files
24
- rspec = dsl.rspec
25
- watch(rspec.spec_helper) { rspec.spec_dir }
26
- watch(rspec.spec_support) { rspec.spec_dir }
27
- watch(rspec.spec_files)
28
-
29
- # Ruby files
30
- ruby = dsl.ruby
31
- dsl.watch_spec_files_for(ruby.lib_files)
32
-
33
-
34
- # watch(rails.routes) { "#{rspec.spec_dir}/routing" }
35
- # watch(rails.app_controller) { "#{rspec.spec_dir}/controllers" }
36
- end
data/Rakefile DELETED
@@ -1,8 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'bundler/gem_tasks'
4
- require 'rspec/core/rake_task'
5
-
6
- RSpec::Core::RakeTask.new(:spec)
7
-
8
- task default: :spec
data/bin/console DELETED
@@ -1,14 +0,0 @@
1
- #!/usr/bin/env ruby
2
-
3
- require 'bundler/setup'
4
- require 'typerb'
5
-
6
- # You can add fixtures and/or initialization code here to make experimenting
7
- # with your gem easier. You can also use a different console, if you like.
8
-
9
- # (If you use this, don't forget to add pry to your Gemfile!)
10
- require 'pry'
11
- Pry.start
12
-
13
- # require "irb"
14
- # IRB.start(__FILE__)
data/bin/setup DELETED
@@ -1,8 +0,0 @@
1
- #!/usr/bin/env bash
2
- set -euo pipefail
3
- IFS=$'\n\t'
4
- set -vx
5
-
6
- bundle install
7
-
8
- # Do any other automated setup that you need to do here
data/typerb.gemspec DELETED
@@ -1,37 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- lib = File.expand_path('lib', __dir__)
4
- $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
- require 'typerb/version'
6
-
7
- Gem::Specification.new do |spec|
8
- spec.name = 'typerb'
9
- spec.version = Typerb::VERSION
10
- spec.authors = ['Oleg Antonyan']
11
- spec.email = ['oleg.b.antonyan@gmail.com']
12
-
13
- spec.summary = 'Typecheck sugar for Ruby.'
14
- spec.description = 'Typecheck sugar for Ruby.'
15
- spec.homepage = 'https://github.com/olegantonyan/typerb'
16
- spec.license = 'MIT'
17
-
18
- # Specify which files should be added to the gem when it is released.
19
- # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
20
- spec.files = Dir.chdir(File.expand_path(__dir__)) do
21
- `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
22
- end
23
- spec.bindir = 'exe'
24
- spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
25
- spec.require_paths = ['lib']
26
-
27
- spec.add_development_dependency 'bundler', '>= 1.17'
28
- spec.add_development_dependency 'guard'
29
- spec.add_development_dependency 'guard-rspec'
30
- spec.add_development_dependency 'pry'
31
- spec.add_development_dependency 'rake', '>= 10.0'
32
- spec.add_development_dependency 'rspec', '>= 3.0'
33
- spec.add_development_dependency 'rubocop'
34
- spec.add_development_dependency 'super_awesome_print'
35
-
36
- spec.required_ruby_version = '>= 2.4'
37
- end