purist 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 0eb6e6c4be218cd0e3d351a6767860f6a5da8952f15e3ee740aec9ff1c28882b
4
+ data.tar.gz: 7a539fd18fc3361183007c48daca2a226267a1b93189a16cdb23bfff4ef7bc6b
5
+ SHA512:
6
+ metadata.gz: d9e7453e526c7db2f523e70a9c6c90cc11bfc2045aeea49ff0d6e921c76127f55c76f6ba06ebaad684f15bd7b6574312a4cc781d013763c43ae042c3128a35b8
7
+ data.tar.gz: 3f4a5994752f455d13831dcdf5317f6a8b71c1748f2c4a38857199250ba1674337388fd3399ae3b4da1221ab39819b1dc90ee1243106bf3b192de650c911c982
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2024 viralpraxis
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,117 @@
1
+ # Purst
2
+
3
+ Purist is a tool designed to help detecting impure ruby code in runtime.
4
+
5
+ Ruby's stdlib and corelibs include a bunch of impure methods, including
6
+
7
+ 1. randomization: `Kernel.rand`, `Random.rand`, `SecureRandom.hex` and so on
8
+
9
+ 2. IO-related methods like `Kernel.readline` or `IO.popen`
10
+
11
+ 3. specialized side-effects like `Kernel.fork` or `Kernel.syscall`
12
+
13
+ Purist hooks into ruby's `tracepoint` API to detect any invocation of these methods.
14
+ You can see the full list of target methods in `configuration.rb` source file.
15
+
16
+ ## Installation
17
+
18
+ Install the gem and add to the application's Gemfile by executing:
19
+
20
+ $ bundle add purist
21
+
22
+ If bundler is not being used to manage dependencies, install the gem by executing:
23
+
24
+ $ gem install purist
25
+
26
+ ## Usage
27
+
28
+ To check if your code is pure, simply pass it into `Purist.trace` method:
29
+
30
+ ```ruby
31
+ Purist.trace { 3 * 3 } # 9
32
+ ```
33
+
34
+ If provided block is impure, an exception will be raised:
35
+
36
+ ```ruby
37
+ irb(main):001> Purist.trace { p "I'm impure" }
38
+ gems/purist/lib/purist/handler.rb:23:in `call': {:path=>"(irb)", :lineno=>1, :module_name=>Kernel, :method_name=>:p} (Purist::Errors::PurityViolationError)
39
+ ```
40
+
41
+ You can retrieve exception details like this:
42
+
43
+ ```ruby
44
+ exception = Purist.trace { p 1 } rescue $!
45
+
46
+ p exception.trace_point
47
+
48
+ {
49
+ :path => ".../zeitwerk-2.6.13/lib/zeitwerk/kernel.rb",
50
+ :lineno => 23,
51
+ :module_name => Kernel,
52
+ :method_name => :require,
53
+ :backtrace => [...]
54
+ }
55
+ ```
56
+
57
+ ### RSpec integration
58
+
59
+ Purist comes with built-in `RSpec` integration. To enable it, add `require "purist/integrations/rspec"` to your
60
+ `spec_helper.rb` and manually include `Purist::Integrations::RSpec::Matchers`:
61
+
62
+ ```ruby
63
+ require "purist/integrations/rspec"
64
+
65
+ ...
66
+
67
+ RSpec.configure do |config|
68
+ ...
69
+ config.include Purist::Integrations::RSpec::Matchers
70
+ ...
71
+ end
72
+ ```
73
+
74
+ And not `be_pure` and `be_impure` matchers are available:
75
+
76
+ ```ruby
77
+ expect { Module.new }.to be_pure
78
+ expect { User.where(name: :john) }.to be_impure
79
+ ```
80
+
81
+ ### Caveats
82
+
83
+ 1. Passing `Purist.trace` check does not mean your function is totally pure, for instance
84
+
85
+ ```ruby
86
+ def foo(n)
87
+ if n > 0 # pure branch
88
+ n.succ
89
+ else # impure branch
90
+ p n
91
+ end
92
+ end
93
+
94
+ Purist.trace { foo(3) } # 4
95
+ ```
96
+
97
+ 2. Ruby stdlib/corelib is quite big, I'm pretty sure some impure functions are missing from the list.
98
+
99
+ 3. Obviously, Purist is unable to detect anything within 3rd-party C extensions.
100
+
101
+ ## Development
102
+
103
+ 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.
104
+
105
+ 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 the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
106
+
107
+ ## Contributing
108
+
109
+ Bug reports and pull requests are welcome on GitHub at https://github.com/viralpraxis/purist. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/viralpraxis/purist/blob/main/CODE_OF_CONDUCT.md).
110
+
111
+ ## License
112
+
113
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
114
+
115
+ ## Code of Conduct
116
+
117
+ Everyone interacting in the Purist project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/viralpraxis/purist/blob/main/CODE_OF_CONDUCT.md).
@@ -0,0 +1,187 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'singleton'
4
+
5
+ module Purist
6
+ class Configuration # rubocop:disable Metrics/ClassLength
7
+ include Singleton
8
+
9
+ Subject = Class.new do
10
+ def self.from_module(module_name:, method_names:, with_singleton_class: false)
11
+ targets = {}
12
+
13
+ method_names.each do |method_name|
14
+ targets[[module_name, method_name]] = true
15
+ targets[[module_name.singleton_class, method_name]] = true if with_singleton_class
16
+ end
17
+
18
+ targets
19
+ end
20
+ end
21
+
22
+ STDLIB_RANDOM_CLASS =
23
+ if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new('3.0.0')
24
+ Random::Base
25
+ else
26
+ Random
27
+ end
28
+
29
+ TRACE_TARGETS = {
30
+ **Subject.from_module(
31
+ module_name: Kernel,
32
+ with_singleton_class: true,
33
+ method_names: %i[
34
+ abort
35
+ at_exit
36
+ exit
37
+ exit!
38
+
39
+ pp
40
+ gets
41
+ open
42
+ p
43
+ print
44
+ printf
45
+ putc
46
+ puts
47
+ readline
48
+ readlines
49
+ select
50
+
51
+ set_trace_func
52
+ trace_var
53
+ untrace_var
54
+
55
+ `
56
+ exec
57
+ fork
58
+ spawn
59
+ system
60
+
61
+ load
62
+ require
63
+ require_relative
64
+
65
+ rand
66
+ srand
67
+
68
+ sleep
69
+ sprintf
70
+ syscall
71
+ trap
72
+
73
+ autoload
74
+ warn
75
+ ]
76
+ ),
77
+ **Subject.from_module(
78
+ module_name: IO.singleton_class,
79
+ method_names: %i[
80
+ new
81
+ for_fd
82
+ open
83
+ pipe
84
+ popen
85
+ select
86
+
87
+ binread
88
+ read
89
+ readlines
90
+
91
+ binwrite
92
+ write
93
+
94
+ foreach
95
+
96
+ copy_stream
97
+ try_convert
98
+
99
+ sysopen
100
+ ]
101
+ ),
102
+ **Subject.from_module(
103
+ module_name: IO,
104
+ method_names: %i[
105
+ getbyte
106
+ getc
107
+ gets
108
+ pread
109
+ read
110
+ read_nonblock
111
+ readbyte
112
+ readchar
113
+ readline
114
+ readlines
115
+ readpartial
116
+
117
+ print
118
+ printf
119
+ putc
120
+ puts
121
+ pwrite
122
+ write
123
+ write_nonblock
124
+
125
+ pos
126
+ pos=
127
+ ropen
128
+ rewind
129
+ seek
130
+
131
+ each
132
+ each_byte
133
+ each_char
134
+ each_codepoint
135
+
136
+ autoclose=
137
+ binmode
138
+ close
139
+ close_on_exec
140
+ close_read
141
+ close_write
142
+ set_encoding
143
+ set_encoding_by_bom
144
+ sync=
145
+
146
+ fdatasync
147
+ flush
148
+ fsync
149
+ ungetbyte
150
+ ungetc
151
+
152
+ advise
153
+ fcntl
154
+ ioctl
155
+ sysread
156
+ sysseek
157
+ syswrite
158
+ ]
159
+ ),
160
+ **Subject.from_module(
161
+ module_name: Random.singleton_class,
162
+ with_singleton_class: true,
163
+ method_names: %i[
164
+ bytes
165
+ rand
166
+ srand
167
+ urandom
168
+ ]
169
+ ),
170
+ **Subject.from_module(
171
+ module_name: STDLIB_RANDOM_CLASS,
172
+ method_names: %i[
173
+ bytes
174
+ rand
175
+ ]
176
+ )
177
+ }.freeze
178
+
179
+ def trace_targets
180
+ TRACE_TARGETS
181
+ end
182
+
183
+ def action_on_purity_violation
184
+ :raise
185
+ end
186
+ end
187
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Purist
4
+ module Errors
5
+ class Error < StandardError; end
6
+
7
+ class PurityViolationError < Error
8
+ attr_reader :trace_point
9
+
10
+ def initialize(message = nil, trace_point:)
11
+ @trace_point = trace_point
12
+
13
+ super(message || trace_point.reject { |key| key == :backtrace }.inspect)
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'trace_point_slice'
4
+
5
+ module Purist
6
+ module Handler
7
+ def self.build(mode)
8
+ case mode&.to_sym
9
+ when :raise then Raise.new
10
+ when nil then nil
11
+ else raise ArgumentError, "Unexpected mode `#{mode.inspect}`"
12
+ end
13
+ end
14
+
15
+ class Base; end # rubocop:disable Lint/EmptyClass
16
+
17
+ class Raise < Base
18
+ def call(trace_point)
19
+ raise Purist::Errors::PurityViolationError.new(
20
+ trace_point: TracePointSlice.call(trace_point)
21
+ )
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec::Matchers.define_negated_matcher :be_impure, :be_pure
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Purist
4
+ module Integrations
5
+ module RSpec
6
+ class BePure
7
+ def supports_value_expectations?
8
+ false
9
+ end
10
+
11
+ def supports_block_expectations?
12
+ true
13
+ end
14
+
15
+ def matches?(block)
16
+ Purist.trace { block.call }
17
+
18
+ true
19
+ rescue Purist::Errors::PurityViolationError => e
20
+ @purist_exception = e
21
+
22
+ false
23
+ end
24
+
25
+ def description
26
+ 'block to be pure'
27
+ end
28
+
29
+ def description_when_negated
30
+ 'block to be impure'
31
+ end
32
+
33
+ def failure_message
34
+ "expected #{description}, detected impure #{purist_exception.trace_point.inspect}"
35
+ end
36
+
37
+ def failure_message_when_negated
38
+ "expected #{description_when_negated}, but no side-effects were detected"
39
+ end
40
+
41
+ private
42
+
43
+ attr_reader :purist_exception
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'be_pure'
4
+ require_relative 'be_impure'
5
+
6
+ module Purist
7
+ module Integrations
8
+ module RSpec
9
+ module Matchers
10
+ def be_pure
11
+ BePure.new
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'rspec/matchers'
4
+ require 'rspec/matchers'
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Purist
4
+ class Matcher
5
+ def self.match?(specification, trace_point)
6
+ specification.key? [trace_point.defined_class, trace_point.callee_id]
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Purist
4
+ class TracePointSlice
5
+ def self.call(trace_point)
6
+ {
7
+ path: trace_point.path,
8
+ lineno: trace_point.lineno,
9
+ module_name: trace_point.defined_class,
10
+ method_name: trace_point.callee_id,
11
+ backtrace: trace_point.binding.send(:caller)
12
+ }
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Purist
4
+ VERSION = '1.0.0'
5
+ end
data/lib/purist.rb ADDED
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'purist/configuration'
4
+ require_relative 'purist/errors'
5
+ require_relative 'purist/handler'
6
+ require_relative 'purist/matcher'
7
+ require_relative 'purist/version'
8
+
9
+ module Purist
10
+ TRACE_POINT_TYPES = %i[call c_call].freeze
11
+ TRACE_HANDLER = proc do |trace_point|
12
+ next unless Purist::Matcher.match?(configuration.trace_targets, trace_point)
13
+
14
+ Purist.handler.call(trace_point)
15
+ end
16
+
17
+ def self.trace(&block)
18
+ return unless block
19
+
20
+ TracePoint
21
+ .new(*TRACE_POINT_TYPES, &TRACE_HANDLER)
22
+ .enable(&block)
23
+ end
24
+
25
+ def self.handler
26
+ @handler ||= Purist::Handler.build(configuration.action_on_purity_violation)
27
+ end
28
+
29
+ def self.configuration
30
+ @configuration ||= Purist::Configuration.instance
31
+ end
32
+ end
metadata ADDED
@@ -0,0 +1,63 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: purist
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Yaroslav Kurbatov
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2024-03-10 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: |
14
+ Automatic runtime impure ruby methods invocation detection by tracing predifined
15
+ Ruby stdlib and core libs methods via `tracepoint` API
16
+ email: iaroslav2k@gmail.com
17
+ executables: []
18
+ extensions: []
19
+ extra_rdoc_files:
20
+ - README.md
21
+ - LICENSE.txt
22
+ files:
23
+ - LICENSE.txt
24
+ - README.md
25
+ - lib/purist.rb
26
+ - lib/purist/configuration.rb
27
+ - lib/purist/errors.rb
28
+ - lib/purist/handler.rb
29
+ - lib/purist/integrations/rspec.rb
30
+ - lib/purist/integrations/rspec/be_impure.rb
31
+ - lib/purist/integrations/rspec/be_pure.rb
32
+ - lib/purist/integrations/rspec/matchers.rb
33
+ - lib/purist/matcher.rb
34
+ - lib/purist/trace_point_slice.rb
35
+ - lib/purist/version.rb
36
+ homepage: https://github.com/viralpraxis/purist
37
+ licenses:
38
+ - MIT
39
+ metadata:
40
+ homepage_uri: https://github.com/viralpraxis/purist
41
+ source_code_uri: https://github.com/viralpraxis/purist/tree/main
42
+ changelog_uri: https://github.com/viralpraxis/purist/blob/main/CHANGELOG.md
43
+ rubygems_mfa_required: 'true'
44
+ post_install_message:
45
+ rdoc_options: []
46
+ require_paths:
47
+ - lib
48
+ required_ruby_version: !ruby/object:Gem::Requirement
49
+ requirements:
50
+ - - ">="
51
+ - !ruby/object:Gem::Version
52
+ version: 2.7.0
53
+ required_rubygems_version: !ruby/object:Gem::Requirement
54
+ requirements:
55
+ - - ">="
56
+ - !ruby/object:Gem::Version
57
+ version: '0'
58
+ requirements: []
59
+ rubygems_version: 3.5.3
60
+ signing_key:
61
+ specification_version: 4
62
+ summary: Automatic runtime impure ruby methods invocation detection
63
+ test_files: []