typerb 0.3.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: 0d36da771caf7376c9ade13b380b77ae20cd50ac03172c86e6d54f3c54af5414
4
- data.tar.gz: 1bb04d2199a1384049168302010c8a8d0e5114301e26108167d8d54810f4c79a
3
+ metadata.gz: 548c823e7f9d7185d069b2bb18f370bdcb8b18603e82de22b78712c251dc4a14
4
+ data.tar.gz: 637a184fa56802fdb93a60020e7eb633692e606726215dce533de62367edd3fe
5
5
  SHA512:
6
- metadata.gz: 79fa49f81f729c9e760afe1cd4abf5e2d259b1ed48a711ff64732a1411219c2d6c6fd2154dc5386e3b12d4f7cd607d4f612e06908938802ae6890c2cfd0029be
7
- data.tar.gz: 5f405c7242a53f27925df6981944cc31f57b7bad60077aa36c9f7c08fd6a6a428998caca93ddf3f124829c5b40254f840bcbdf183d20ce19b41caf7b2438ebfa
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
- [![Build Status](https://travis-ci.org/olegantonyan/typerb.svg?branch=master)](https://travis-ci.org/olegantonyan/typerb)
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
@@ -20,29 +20,35 @@ class A
20
20
  def call_with_enum(arg)
21
21
  arg.enum!(:one, :two)
22
22
  end
23
+
24
+ def call_with_subset(arg)
25
+ arg.subset_of!(%i[one two])
26
+ end
23
27
  end
24
28
 
25
- A.new.call(1) #=> TypeError: `some_arg` should be String or Symbol, not Integer
26
- A.new.call_with_respond_checks(1) #=> TypeError: 'Integer should respond to all methods: strip'
27
- A.new.call_with_enum(:three) #=> TypeError: 'Symbol (`arg`) should be one of: [one, two], not 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]
28
33
  ```
29
34
 
30
35
  This is equivalent to:
36
+
31
37
  ```ruby
32
38
  class A
33
39
  def call(some_arg)
34
- 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)
35
41
  end
36
42
 
37
43
  def call_with_respond_checks(some_arg)
38
- 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) }
39
45
  end
40
46
  end
41
47
  ```
42
48
 
43
- But without boilerplate.
49
+ But without the boilerplate.
44
50
 
45
- It also has `not_nil!` method, similar to Crystal language.
51
+ There is also a `not_nil!` method, similar to the Crystal language.
46
52
 
47
53
  ```ruby
48
54
  class A
@@ -53,13 +59,15 @@ class A
53
59
  end
54
60
  end
55
61
 
56
- A.new.call(nil) #=> TypeError: expected not nil, but got nil
62
+ A.new.call(nil) #=> TypeError: `some_arg` should not be nil
57
63
  ```
58
64
 
65
+ Every method returns `self` when the check passes, so checks can be chained or inlined into assignments.
66
+
59
67
  ## Why?
60
68
 
61
- 1. Catch error as early as possible (especially nils);
62
- 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.
63
71
 
64
72
  ## Installation
65
73
 
@@ -77,20 +85,9 @@ Or install it yourself as:
77
85
 
78
86
  $ gem install typerb
79
87
 
80
- If this fails with error
81
- ```
82
- ERROR: Error installing typerb:
83
- There are no versions of typerb (>= 0) compatible with your Ruby & RubyGems
84
- typerb requires Ruby version >= 2.6.0.pre.preview3. The current ruby version is 2.6.0.
85
- ```
86
- even when you have Ruby 2.6.0-preview3 installed, then try installing it through Gemfile from git:
87
- ```ruby
88
- gem 'typerb', github: 'olegantonyan/typerb'
89
- ```
90
-
91
88
  ## Usage
92
89
 
93
- 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.
94
91
  2. Call `.type!()` on any object to assert its type.
95
92
  3. PROFIT! No more "NoMethodError for nil" 10 methods up the stack. You'll know exactly where this nil came from.
96
93
 
@@ -107,33 +104,44 @@ class A
107
104
  end
108
105
  ```
109
106
 
110
- 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.
111
112
 
112
- This refinement adds `type!()` and `not_nil!` methods to `Object` class so you can call it on almost much any object (except those inherited from `BasicObject`, but these are rare).
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.
113
119
 
114
- 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).
120
+ | Ruby | Parser |
121
+ | --------- | ---------------------------- |
122
+ | 3.3+ | Prism |
123
+ | 3.0 - 3.2 | `RubyVM::AbstractSyntaxTree` |
124
+
125
+ Both parsers produce the same messages, and CI runs the suite against every supported version.
115
126
 
116
127
  ## Limitations
117
128
 
118
- 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.
119
130
 
120
- Known limitations:
131
+ 1. Several checks on the same line - there is no way to tell which one raised:
121
132
 
122
- 1. Multi-line method call:
123
133
  ```ruby
124
134
  class A
125
135
  using Typerb
126
136
 
127
- def call(some_arg)
128
- some_arg.
129
- type!(String)
130
- # 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
131
- # some_arg. type!(String) is ok though
137
+ def initialize(arg1, arg2)
138
+ arg1.type!(Integer); arg2.type!(String)
132
139
  end
133
140
  end
134
141
  ```
135
142
 
136
- 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
+
137
145
  ```ruby
138
146
  [1] pry(main)> class A
139
147
  [1] pry(main)* using Typerb
@@ -142,34 +150,26 @@ end
142
150
  [1] pry(main)* end
143
151
  [1] pry(main)* end
144
152
  [2] pry(main)> A.new.call(1)
145
- TypeError: expected Hash, got Integer
146
- # here we cannot get the source code for a line containing "a.type!(Hash)", so cannot see the variable name
147
- ```
148
-
149
- 3. Multiple arguments on the same line:
150
- ```ruby
151
- class A
152
- using Typerb
153
-
154
- def initialize(arg1, arg2)
155
- arg1.type!(Integer); arg2.type!(String)
156
- # no way to tell the variable - raise error message without variable name
157
- # same error will be raised on Ruby < 2.6.0 because there is no RubyVM::AST
158
- end
159
- end
153
+ TypeError: expected Hash, got Integer (1)
160
154
  ```
161
155
 
162
- 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.
163
157
 
164
158
  ## Development
165
159
 
166
- 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.
167
162
 
168
- 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).
169
167
 
170
168
  ## Contributing
171
169
 
172
- 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.
173
173
 
174
174
  ## License
175
175
 
@@ -177,4 +177,5 @@ The gem is available as open source under the terms of the [MIT License](https:/
177
177
 
178
178
  ## Code of Conduct
179
179
 
180
- 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,23 +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
3
+ require 'typerb/variable_name'
13
4
 
14
- def elements_text(elements)
15
- '[' + elements.join(', ') + ']'
16
- end
17
- end
18
-
19
- def raise_with(backtrace, exception_text)
20
- 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))
21
9
  exception.set_backtrace(backtrace)
22
10
  raise exception
23
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.3.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,54 +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
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
44
+ end
45
+
46
+ def subset_of!(superset)
47
+ raise ArgumentError, 'receiver must be Enumerable' unless is_a?(Enumerable)
48
+ raise ArgumentError, 'superset must be Enumerable' unless superset.is_a?(Enumerable)
49
+
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?
59
53
 
60
- 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
61
57
  end
62
58
  end
63
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.3.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: 2021-06-10 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,37 +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
- - ".gitignore"
133
- - ".rspec"
134
- - ".rubocop.yml"
135
- - ".ruby-version"
136
- - ".travis.yml"
62
+ - CHANGELOG.md
137
63
  - CODE_OF_CONDUCT.md
138
- - Gemfile
139
- - Gemfile.lock
140
- - Guardfile
141
64
  - LICENSE.txt
142
65
  - README.md
143
- - Rakefile
144
- - bin/console
145
- - bin/setup
146
66
  - lib/typerb.rb
147
67
  - lib/typerb/exceptional.rb
68
+ - lib/typerb/prism_parser.rb
69
+ - lib/typerb/ruby_vm_parser.rb
70
+ - lib/typerb/source_cache.rb
148
71
  - lib/typerb/variable_name.rb
149
72
  - lib/typerb/version.rb
150
- - typerb.gemspec
151
73
  homepage: https://github.com/olegantonyan/typerb
152
74
  licenses:
153
75
  - MIT
154
- metadata: {}
155
- 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'
156
81
  rdoc_options: []
157
82
  require_paths:
158
83
  - lib
@@ -160,15 +85,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
160
85
  requirements:
161
86
  - - ">="
162
87
  - !ruby/object:Gem::Version
163
- version: '2.4'
88
+ version: '3.0'
164
89
  required_rubygems_version: !ruby/object:Gem::Requirement
165
90
  requirements:
166
91
  - - ">="
167
92
  - !ruby/object:Gem::Version
168
93
  version: '0'
169
94
  requirements: []
170
- rubygems_version: 3.0.3
171
- signing_key:
95
+ rubygems_version: 4.0.16
172
96
  specification_version: 4
173
97
  summary: Typecheck sugar for Ruby.
174
98
  test_files: []
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,21 +0,0 @@
1
- AllCops:
2
- TargetRubyVersion: 2.4
3
- Exclude:
4
- - 'bin/**/*'
5
- - 'Guardfile'
6
-
7
- inherit_mode:
8
- merge:
9
- - Exclude
10
-
11
- Style/CommentedKeyword:
12
- Enabled: false
13
-
14
- Style/Documentation:
15
- Enabled: false
16
-
17
- Style/Semicolon:
18
- Enabled: false
19
-
20
- Metrics/LineLength:
21
- Max: 170
data/.ruby-version DELETED
@@ -1 +0,0 @@
1
- 2.6.6
data/.travis.yml DELETED
@@ -1,15 +0,0 @@
1
- ---
2
- sudo: false
3
- language: ruby
4
- cache: bundler
5
- before_install: gem install bundler
6
-
7
- rvm:
8
- - 2.4.0
9
- - 2.5.3
10
- - 2.6.0
11
- - 2.6.1
12
-
13
- script:
14
- - bundle exec rubocop -DP
15
- - bundle exec rspec
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,94 +0,0 @@
1
- PATH
2
- remote: .
3
- specs:
4
- typerb (0.3.0)
5
-
6
- GEM
7
- remote: https://rubygems.org/
8
- specs:
9
- ast (2.4.0)
10
- awesome_print (1.8.0)
11
- coderay (1.1.2)
12
- diff-lcs (1.3)
13
- ffi (1.11.1)
14
- formatador (0.2.5)
15
- guard (2.15.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.9.12)
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
- jaro_winkler (1.5.3)
30
- listen (3.1.5)
31
- rb-fsevent (~> 0.9, >= 0.9.4)
32
- rb-inotify (~> 0.9, >= 0.9.7)
33
- ruby_dep (~> 1.2)
34
- lumberjack (1.0.13)
35
- method_source (0.9.2)
36
- nenv (0.3.0)
37
- notiffany (0.1.1)
38
- nenv (~> 0.1)
39
- shellany (~> 0.0)
40
- parallel (1.17.0)
41
- parser (2.6.3.0)
42
- ast (~> 2.4.0)
43
- pry (0.12.2)
44
- coderay (~> 1.1.0)
45
- method_source (~> 0.9.0)
46
- rainbow (3.0.0)
47
- rake (12.3.2)
48
- rb-fsevent (0.10.3)
49
- rb-inotify (0.10.0)
50
- ffi (~> 1.0)
51
- rspec (3.8.0)
52
- rspec-core (~> 3.8.0)
53
- rspec-expectations (~> 3.8.0)
54
- rspec-mocks (~> 3.8.0)
55
- rspec-core (3.8.1)
56
- rspec-support (~> 3.8.0)
57
- rspec-expectations (3.8.4)
58
- diff-lcs (>= 1.2.0, < 2.0)
59
- rspec-support (~> 3.8.0)
60
- rspec-mocks (3.8.1)
61
- diff-lcs (>= 1.2.0, < 2.0)
62
- rspec-support (~> 3.8.0)
63
- rspec-support (3.8.2)
64
- rubocop (0.72.0)
65
- jaro_winkler (~> 1.5.1)
66
- parallel (~> 1.10)
67
- parser (>= 2.6)
68
- rainbow (>= 2.2.2, < 4.0)
69
- ruby-progressbar (~> 1.7)
70
- unicode-display_width (>= 1.4.0, < 1.7)
71
- ruby-progressbar (1.10.1)
72
- ruby_dep (1.5.0)
73
- shellany (0.0.1)
74
- super_awesome_print (0.2.5)
75
- awesome_print
76
- thor (0.20.3)
77
- unicode-display_width (1.6.0)
78
-
79
- PLATFORMS
80
- ruby
81
-
82
- DEPENDENCIES
83
- bundler (>= 1.17)
84
- guard
85
- guard-rspec
86
- pry
87
- rake (>= 10.0)
88
- rspec (>= 3.0)
89
- rubocop
90
- super_awesome_print
91
- typerb!
92
-
93
- BUNDLED WITH
94
- 1.17.2
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