├── .rspec ├── lib ├── abstriker │ └── version.rb └── abstriker.rb ├── .travis.yml ├── Rakefile ├── bin ├── setup └── console ├── Gemfile ├── .gitignore ├── spec ├── spec_helper.rb └── abstriker_spec.rb ├── abstriker.gemspec ├── LICENSE.txt └── README.md /.rspec: -------------------------------------------------------------------------------- 1 | --format documentation 2 | --color 3 | --require spec_helper 4 | -------------------------------------------------------------------------------- /lib/abstriker/version.rb: -------------------------------------------------------------------------------- 1 | module Abstriker 2 | VERSION = "0.1.3" 3 | end 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: ruby 3 | rvm: 4 | - 2.6.2 5 | - 2.5.2 6 | before_install: gem install bundler -v 2.0.1 7 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "rspec/core/rake_task" 3 | 4 | RSpec::Core::RakeTask.new(:spec) 5 | 6 | task :default => :spec 7 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 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 9 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | git_source(:github) {|repo_name| "https://github.com/#{repo_name}" } 4 | 5 | # Specify your gem's dependencies in abstriker.gemspec 6 | gemspec 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /_yardoc/ 4 | /coverage/ 5 | /doc/ 6 | /pkg/ 7 | /spec/reports/ 8 | /tmp/ 9 | 10 | Gemfile.lock 11 | 12 | # rspec failure tracking 13 | .rspec_status 14 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "bundler/setup" 4 | require "abstriker" 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__) 15 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require "bundler/setup" 2 | require "abstriker" 3 | 4 | RSpec.configure do |config| 5 | # Enable flags like --only-failures and --next-failure 6 | config.example_status_persistence_file_path = ".rspec_status" 7 | 8 | # Disable RSpec exposing methods globally on `Module` and `main` 9 | config.disable_monkey_patching! 10 | 11 | config.expect_with :rspec do |c| 12 | c.syntax = :expect 13 | end 14 | 15 | config.before(:suite) do 16 | GC.disable 17 | end 18 | 19 | config.after(:suite) do 20 | ObjectSpace.each_object do |obj| 21 | if obj.is_a?(TracePoint) 22 | raise "exists enabled TracePoint" if obj.enabled? 23 | end 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /abstriker.gemspec: -------------------------------------------------------------------------------- 1 | 2 | lib = File.expand_path("../lib", __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require "abstriker/version" 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = "abstriker" 8 | spec.version = Abstriker::VERSION 9 | spec.authors = ["joker1007"] 10 | spec.email = ["kakyoin.hierophant@gmail.com"] 11 | 12 | spec.summary = %q{Add abstract syntax that target method requires subclass implementation.} 13 | spec.description = %q{Add abstract syntax that target method requires subclass implementation.} 14 | spec.homepage = "https://github.com/joker1007/abstriker" 15 | spec.license = "MIT" 16 | 17 | spec.files = `git ls-files -z`.split("\x0").reject do |f| 18 | f.match(%r{^(test|spec|features)/}) 19 | end 20 | spec.bindir = "exe" 21 | spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } 22 | spec.require_paths = ["lib"] 23 | 24 | spec.add_development_dependency "rake", "~> 10.0" 25 | spec.add_development_dependency "rspec", "~> 3.0" 26 | end 27 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 joker1007 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. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Abstriker 2 | [![Build Status](https://travis-ci.org/joker1007/abstriker.svg?branch=master)](https://travis-ci.org/joker1007/abstriker) 3 | [![Gem Version](https://badge.fury.io/rb/abstriker.svg)](https://badge.fury.io/rb/abstriker) 4 | 5 | This gem adds `abstract` syntax. that is similar to Java's one. 6 | `abstract` modified method requires subclass implementation. 7 | 8 | If subclass does not implement `abstract` method, raise `Abstriker::NotImplementedError`. 9 | `Abstriker::NotImplementedError` is currently subclass of `::NotImplementedError`. 10 | 11 | This gem is pseudo static code analyzer by `TracePoint` and `Ripper`. 12 | 13 | it detect abstract violation when class(module) is defined, not runtime. 14 | 15 | ### My similar gems 16 | 17 | - [finalist](https://github.com/joker1007/finalist) (`final` implementation) 18 | - [overrider](https://github.com/joker1007/overrider) (`override` implementation) 19 | 20 | ## Installation 21 | 22 | Add this line to your application's Gemfile: 23 | 24 | ```ruby 25 | gem 'abstriker' 26 | ``` 27 | 28 | And then execute: 29 | 30 | $ bundle 31 | 32 | Or install it yourself as: 33 | 34 | $ gem install abstriker 35 | 36 | ## Usage 37 | 38 | ```ruby 39 | class A1 40 | extend Abstriker 41 | 42 | abstract def foo 43 | end 44 | end 45 | 46 | class A3 < A1 47 | def foo 48 | end 49 | end # => OK 50 | 51 | class A2 < A1 52 | end # => raise 53 | 54 | Class.new(A1) do 55 | end # => raise 56 | ``` 57 | 58 | ### for Production 59 | If you want to disable Abstriker, write `Abstriker.disable = true` at first line. 60 | If Abstriker is disabled, TracePoint never runs, and so there is no overhead of VM instruction. 61 | 62 | ### Examples 63 | 64 | #### include module 65 | 66 | ```ruby 67 | module B1 68 | extend Abstriker 69 | 70 | abstract def foo 71 | end 72 | end 73 | 74 | class B2 75 | include B1 76 | end # => raise 77 | 78 | Module.new do 79 | include B1 80 | end # => raise 81 | ``` 82 | 83 | #### include module outer class definition 84 | ```ruby 85 | module A1 86 | extend Abstriker 87 | 88 | abstract def foo 89 | end 90 | end 91 | 92 | class A2; 93 | end 94 | 95 | A2.include(A1) # => raise 96 | ``` 97 | 98 | #### extend module 99 | 100 | ```ruby 101 | module C1 102 | extend Abstriker 103 | 104 | abstract def foo 105 | end 106 | end 107 | 108 | class C3 109 | extend C1 110 | 111 | def self.foo 112 | end 113 | end # => OK 114 | 115 | class C2 116 | extend C1 117 | end # raise 118 | ``` 119 | 120 | #### singleton class 121 | 122 | ```ruby 123 | class D1 124 | extend Abstriker 125 | 126 | class << self 127 | abstract def foo 128 | end 129 | end 130 | end 131 | 132 | class D3 < D1 133 | def self.foo 134 | end 135 | end # => OK 136 | 137 | class D2 < D1 138 | end # => raise 139 | 140 | Class.new(D1) do 141 | end # => raise 142 | 143 | ``` 144 | 145 | ## Development 146 | 147 | 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. 148 | 149 | 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). 150 | 151 | ## Contributing 152 | 153 | Bug reports and pull requests are welcome on GitHub at https://github.com/joker1007/abstriker. 154 | 155 | ## License 156 | 157 | The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT). 158 | -------------------------------------------------------------------------------- /lib/abstriker.rb: -------------------------------------------------------------------------------- 1 | require "abstriker/version" 2 | require "set" 3 | require "ripper" 4 | 5 | module Abstriker 6 | class NotImplementedError < NotImplementedError 7 | attr_reader :subclass, :abstract_method 8 | 9 | def initialize(klass, abstract_method) 10 | super("#{abstract_method} is abstract, but not implemented by #{klass}") 11 | @subclass = klass 12 | @abstract_method = abstract_method 13 | end 14 | end 15 | 16 | class SexpTraverser 17 | def initialize(sexp) 18 | @sexp = sexp 19 | end 20 | 21 | def traverse(current_sexp = nil, parent = nil, &block) 22 | sexp = current_sexp || @sexp 23 | first = sexp[0] 24 | if first.is_a?(Symbol) # node 25 | yield sexp, parent 26 | args = Ripper::PARSER_EVENT_TABLE[first] 27 | return if args.nil? || args.zero? 28 | 29 | args.times do |i| 30 | param = sexp[i + 1] 31 | if param.is_a?(Array) 32 | traverse(param, sexp, &block) 33 | end 34 | end 35 | else # array 36 | sexp.each do |n| 37 | if n.is_a?(Array) 38 | traverse(n, sexp, &block) 39 | end 40 | end 41 | end 42 | end 43 | end 44 | 45 | @disable = false 46 | 47 | def self.disable=(v) 48 | @disable = v 49 | end 50 | 51 | def self.disabled? 52 | @disable 53 | end 54 | 55 | def self.enabled? 56 | !disabled? 57 | end 58 | 59 | def self.abstract_methods 60 | @abstract_methods ||= {} 61 | end 62 | 63 | def self.sexps 64 | @sexps ||= {} 65 | end 66 | 67 | def self.extended(base) 68 | base.extend(SyntaxMethods) 69 | base.singleton_class.extend(SyntaxMethods) 70 | if enabled? 71 | base.extend(ModuleMethods) if base.instance_of?(Module) 72 | base.extend(ClassMethods) if base.instance_of?(Class) 73 | end 74 | end 75 | 76 | module SyntaxMethods 77 | private 78 | 79 | def abstract(symbol) 80 | method_set = Abstriker.abstract_methods[self] ||= Set.new 81 | method_set.add(instance_method(symbol)) 82 | end 83 | 84 | def abstract_singleton_method(symbol) 85 | method_set = Abstriker.abstract_methods[singleton_class] ||= Set.new 86 | method_set.add(singleton_class.instance_method(symbol)) 87 | end 88 | end 89 | 90 | module HookBase 91 | private 92 | 93 | def call_at_outer_class_definition?(klass, trace_event, method_name) 94 | if trace_event.event == :c_return && trace_event.self == klass && trace_event.method_id == method_name 95 | traverser = SexpTraverser.new(Abstriker.sexps[trace_event.path]) 96 | traverser.traverse do |n, parent| 97 | if n[0] == :@ident && n[1] == method_name.to_s && n[2][0] == trace_event.lineno 98 | if parent[0] == :command || parent[0] == :fcall 99 | # include Mod 100 | elsif parent[0] == :command_call || parent[0] == :call 101 | if parent[1][0] == :var_ref && parent[1][1][0] == :@kw && parent[1][1][1] == "self" 102 | # self.include Mod 103 | return false 104 | else 105 | # unknown case 106 | return true 107 | end 108 | else 109 | return true 110 | end 111 | end 112 | end 113 | end 114 | 115 | false 116 | end 117 | 118 | def check_abstract_methods(klass) 119 | return if Abstriker.disabled? 120 | 121 | unless klass.instance_variable_get("@__abstract_trace_point") 122 | tp = TracePoint.trace(:end, :c_return, :raise) do |t| 123 | if t.event == :raise 124 | tp.disable 125 | next 126 | end 127 | 128 | t_self = t.self 129 | 130 | target_class_end = t_self == klass && t.event == :end 131 | target_class_new_end = (t_self == Class || t_self == Module) && t.event == :c_return && t.method_id == :new && t.return_value == klass 132 | include_at_outer = call_at_outer_class_definition?(klass, t, :include) 133 | if target_class_end || target_class_new_end || include_at_outer 134 | klass.ancestors.drop(1).each do |mod| 135 | Abstriker.abstract_methods[mod]&.each do |fmeth| 136 | meth = klass.instance_method(fmeth.name) rescue nil 137 | if meth.nil? || meth.owner == mod 138 | tp.disable 139 | klass.instance_variable_set("@__abstract_trace_point", nil) 140 | raise Abstriker::NotImplementedError.new(klass, fmeth) 141 | end 142 | end 143 | end 144 | tp.disable 145 | klass.instance_variable_set("@__abstract_trace_point", nil) 146 | end 147 | 148 | extend_at_outer = call_at_outer_class_definition?(klass, t, :extend) 149 | if target_class_end || target_class_new_end || extend_at_outer 150 | klass.singleton_class.ancestors.drop(1).each do |mod| 151 | Abstriker.abstract_methods[mod]&.each do |fmeth| 152 | meth = klass.singleton_class.instance_method(fmeth.name) rescue nil 153 | if meth.nil? || meth.owner == mod 154 | tp.disable 155 | klass.instance_variable_set("@__abstract_trace_point", nil) 156 | raise Abstriker::NotImplementedError.new(klass, fmeth) 157 | end 158 | end 159 | end 160 | tp.disable 161 | klass.instance_variable_set("@__abstract_trace_point", nil) 162 | end 163 | end 164 | klass.instance_variable_set("@__abstract_trace_point", tp) 165 | end 166 | end 167 | end 168 | 169 | module ClassMethods 170 | include HookBase 171 | 172 | private 173 | 174 | def inherited(subclass) 175 | check_abstract_methods(subclass) 176 | end 177 | end 178 | 179 | module ModuleMethods 180 | include HookBase 181 | 182 | private 183 | 184 | def included(base) 185 | super 186 | return if Abstriker.disabled? 187 | 188 | caller_info = caller_locations(1, 1)[0] 189 | 190 | unless Abstriker.sexps[caller_info.absolute_path] 191 | Abstriker.sexps[caller_info.absolute_path] ||= Ripper.sexp(File.read(caller_info.absolute_path)) 192 | end 193 | check_abstract_methods(base) 194 | end 195 | alias_method :extended, :included 196 | end 197 | end 198 | -------------------------------------------------------------------------------- /spec/abstriker_spec.rb: -------------------------------------------------------------------------------- 1 | RSpec.describe Abstriker do 2 | context "A1 class has abstract method" do 3 | before do 4 | class A1 5 | extend Abstriker 6 | 7 | abstract def foo 8 | end 9 | end 10 | end 11 | 12 | context "subclass not implement" do 13 | it "raise Abstriker::NotImplementedError", aggregate_failures: true do 14 | ex = nil 15 | begin 16 | class A2 < A1 17 | end 18 | rescue Abstriker::NotImplementedError => e 19 | ex = e 20 | end 21 | 22 | expect(ex).to be_a(Abstriker::NotImplementedError) 23 | expect(ex.subclass).to eq(A2) 24 | expect(ex.abstract_method.owner).to eq(A1) 25 | expect(ex.abstract_method.name).to eq(:foo) 26 | end 27 | end 28 | 29 | context "subclass implement" do 30 | it "does not raise" do 31 | class A3 < A1 32 | def foo 33 | end 34 | end 35 | 36 | class A5 < A3 37 | end 38 | end 39 | end 40 | 41 | context "Class.new(A1) and implement" do 42 | it "does not raise" do 43 | Class.new(A1) do 44 | pr = proc do 45 | end 46 | pr.call 47 | 48 | [1, 2, 3].each do |n| 49 | n * 2 50 | end 51 | 52 | define_method(:hoge) do 53 | puts "hoge" 54 | end 55 | 56 | def foo 57 | "hoge" 58 | end 59 | end 60 | end 61 | end 62 | 63 | context "subclass and raise exception" do 64 | it "does raise the exception" do 65 | ex = nil 66 | begin 67 | class A4 < A1 68 | raise "err" 69 | 70 | def foo 71 | end 72 | end 73 | rescue => e 74 | ex = e 75 | end 76 | 77 | expect(ex).to be_a(RuntimeError) 78 | end 79 | end 80 | 81 | context "Class.new(A1) and raise exception" do 82 | it "does raise the exception" do 83 | ex = nil 84 | begin 85 | Class.new(A1) do 86 | raise "err" 87 | 88 | def foo 89 | end 90 | end 91 | rescue => e 92 | ex = e 93 | end 94 | 95 | expect(ex).to be_a(RuntimeError) 96 | end 97 | end 98 | 99 | context "Class.new(A1) with no block" do 100 | it "raise Abstriker::NotImplementedError" do 101 | ex = nil 102 | begin 103 | Class.new(A1) 104 | rescue Abstriker::NotImplementedError => e 105 | ex = e 106 | end 107 | 108 | expect(ex).to be_a(Abstriker::NotImplementedError) 109 | end 110 | end 111 | 112 | context "subclass include refined module that has implementation" do 113 | module A7 114 | end 115 | 116 | using Module.new { 117 | refine A7 do 118 | def foo 119 | end 120 | end 121 | } 122 | 123 | it "does not raise" do 124 | ex = nil 125 | begin 126 | class A6 < A1 127 | include A7 128 | end 129 | rescue Abstriker::NotImplementedError => e 130 | ex = e 131 | end 132 | 133 | expect(ex).to be_a(Abstriker::NotImplementedError) 134 | expect(ex.subclass).to eq(A6) 135 | expect(ex.abstract_method.name).to eq(:foo) 136 | expect(A6.new.foo).to be_nil 137 | end 138 | end 139 | end 140 | 141 | context "B1 module has abstract method" do 142 | before do 143 | module B1 144 | extend Abstriker 145 | 146 | abstract def foo 147 | end 148 | end 149 | end 150 | 151 | context "class include B1 and not implement" do 152 | it "raise Abstriker::NotImplementedError", aggregate_failures: true do 153 | ex = nil 154 | begin 155 | class B2 156 | include B1 157 | end 158 | rescue Abstriker::NotImplementedError => e 159 | ex = e 160 | end 161 | 162 | expect(ex).to be_a(Abstriker::NotImplementedError) 163 | expect(ex.subclass).to eq(B2) 164 | expect(ex.abstract_method.owner).to eq(B1) 165 | expect(ex.abstract_method.name).to eq(:foo) 166 | end 167 | end 168 | 169 | context "class include B1 and implement" do 170 | it "does not raise" do 171 | class B3 172 | include B1 173 | 174 | def foo 175 | end 176 | end 177 | 178 | class B5 < B3 179 | end 180 | end 181 | end 182 | 183 | context "module include B1 and not implement" do 184 | it "raise Abstriker::NotImplementedError", aggregate_failures: true do 185 | ex = nil 186 | begin 187 | module B4 188 | include B1 189 | end 190 | rescue Abstriker::NotImplementedError => e 191 | ex = e 192 | end 193 | 194 | expect(ex).to be_a(Abstriker::NotImplementedError) 195 | expect(ex.subclass).to eq(B4) 196 | expect(ex.abstract_method.owner).to eq(B1) 197 | expect(ex.abstract_method.name).to eq(:foo) 198 | end 199 | end 200 | 201 | context "module include B1 and implement" do 202 | it "does not raise" do 203 | module B6 204 | include B1 205 | 206 | def foo 207 | end 208 | end 209 | 210 | class B9 211 | include B6 212 | end 213 | end 214 | end 215 | 216 | context "module include B1 (after) and implement" do 217 | it "does not raise" do 218 | module B7 219 | def foo 220 | end 221 | 222 | include B1 223 | end 224 | 225 | module B8 226 | include B7 227 | end 228 | end 229 | end 230 | 231 | context "Class.new include B1 and not implement" do 232 | it "raise Abstriker::NotImplementedError", aggregate_failures: true do 233 | ex = nil 234 | klass = nil 235 | begin 236 | Class.new do 237 | klass = self 238 | include B1 239 | end 240 | rescue Abstriker::NotImplementedError => e 241 | ex = e 242 | end 243 | 244 | expect(ex).to be_a(Abstriker::NotImplementedError) 245 | expect(ex.subclass).to eq(klass) 246 | expect(ex.abstract_method.owner).to eq(B1) 247 | expect(ex.abstract_method.name).to eq(:foo) 248 | end 249 | end 250 | 251 | context "Class.new include B1 and implement" do 252 | it "does not raise" do 253 | Class.new do 254 | include B1 255 | 256 | define_method(:foo) { } 257 | end 258 | end 259 | end 260 | 261 | context "Class.new include B1 (after) and implement" do 262 | it "does not raise" do 263 | Class.new do 264 | define_method(:foo) { } 265 | 266 | include B1 267 | end 268 | end 269 | end 270 | 271 | context "Class.new include B1 implement at parent class" do 272 | it "raise Abstriker::NotImplementedError", aggregate_failures: true do 273 | class B10 274 | def foo 275 | end 276 | end 277 | 278 | ex = nil 279 | klass = nil 280 | begin 281 | Class.new(B10) do 282 | klass = self 283 | include B1 284 | end 285 | rescue Abstriker::NotImplementedError => e 286 | ex = e 287 | end 288 | 289 | expect(ex).to be_a(Abstriker::NotImplementedError) 290 | expect(ex.subclass).to eq(klass) 291 | expect(ex.abstract_method.owner).to eq(B1) 292 | expect(ex.abstract_method.name).to eq(:foo) 293 | end 294 | end 295 | 296 | context "Class.new include B1 implement at prepended module" do 297 | it "does not raise", aggregate_failures: true do 298 | module B11 299 | def foo 300 | end 301 | end 302 | 303 | Class.new do 304 | include B1 305 | prepend B11 306 | end 307 | end 308 | end 309 | 310 | context "class extend B1 and not implement" do 311 | it "raise Abstriker::NotImplementedError", aggregate_failures: true do 312 | ex = nil 313 | begin 314 | class B13 315 | extend B1 316 | end 317 | rescue Abstriker::NotImplementedError => e 318 | ex = e 319 | end 320 | 321 | expect(ex).to be_a(Abstriker::NotImplementedError) 322 | expect(ex.subclass).to eq(B13) 323 | expect(ex.abstract_method.owner).to eq(B1) 324 | expect(ex.abstract_method.name).to eq(:foo) 325 | end 326 | end 327 | 328 | context "class extend B1 and implement" do 329 | it "does not raise" do 330 | class B14 331 | extend B1 332 | 333 | def self.foo 334 | end 335 | end 336 | 337 | class B15 < B14 338 | end 339 | end 340 | end 341 | 342 | context "Class.new extend B1 and not implement" do 343 | it "raise Abstriker::NotImplementedError", aggregate_failures: true do 344 | ex = nil 345 | klass = nil 346 | begin 347 | Class.new do 348 | klass = self 349 | extend B1 350 | end 351 | rescue Abstriker::NotImplementedError => e 352 | ex = e 353 | end 354 | 355 | expect(ex).to be_a(Abstriker::NotImplementedError) 356 | expect(ex.subclass).to eq(klass) 357 | expect(ex.abstract_method.owner).to eq(B1) 358 | expect(ex.abstract_method.name).to eq(:foo) 359 | end 360 | end 361 | 362 | context "Class.new extend B1 and implement" do 363 | it "does not raise" do 364 | klass = Class.new do 365 | def self.foo 366 | end 367 | 368 | extend B1 369 | end 370 | 371 | Class.new(klass) do 372 | class << self 373 | def foo 374 | end 375 | end 376 | end 377 | end 378 | end 379 | 380 | context "class << self include B1 and not implement" do 381 | it "raise Abstriker::NotImplementedError", aggregate_failures: true do 382 | ex = nil 383 | begin 384 | class B16 385 | class << self 386 | include B1 387 | end 388 | end 389 | rescue Abstriker::NotImplementedError => e 390 | ex = e 391 | end 392 | 393 | expect(ex).to be_a(Abstriker::NotImplementedError) 394 | expect(ex.subclass).to eq(B16.singleton_class) 395 | expect(ex.abstract_method.owner).to eq(B1) 396 | expect(ex.abstract_method.name).to eq(:foo) 397 | end 398 | end 399 | 400 | context "class << self include B1 and implement" do 401 | it "does not raise" do 402 | class B17 403 | class << self 404 | def foo 405 | end 406 | 407 | include B1 408 | end 409 | end 410 | 411 | class B18 < B17 412 | end 413 | end 414 | end 415 | 416 | context "Module.new include B1 and not implement" do 417 | it "raise Abstriker::NotImplementedError", aggregate_failures: true do 418 | ex = nil 419 | klass = nil 420 | begin 421 | Module.new do 422 | klass = self 423 | include B1 424 | end 425 | rescue Abstriker::NotImplementedError => e 426 | ex = e 427 | end 428 | 429 | expect(ex).to be_a(Abstriker::NotImplementedError) 430 | expect(ex.subclass).to eq(klass) 431 | expect(ex.abstract_method.owner).to eq(B1) 432 | expect(ex.abstract_method.name).to eq(:foo) 433 | end 434 | end 435 | 436 | context "Module.new include B1 and implement" do 437 | it "does not raise" do 438 | Module.new do 439 | include B1 440 | 441 | define_method(:foo) { } 442 | end 443 | end 444 | end 445 | 446 | context "Module.new include B1 (after) and implement" do 447 | it "does not raise" do 448 | Module.new do 449 | define_method(:foo) { } 450 | 451 | include B1 452 | end 453 | end 454 | end 455 | 456 | context "Module.new include B1 implement at prepended module" do 457 | it "does not raise", aggregate_failures: true do 458 | module B12 459 | def foo 460 | end 461 | end 462 | 463 | Module.new do 464 | include B1 465 | prepend B12 466 | end 467 | end 468 | end 469 | 470 | context "module extend B1 and not implement" do 471 | it "raise Abstriker::NotImplementedError", aggregate_failures: true do 472 | ex = nil 473 | begin 474 | module B19 475 | extend B1 476 | end 477 | rescue Abstriker::NotImplementedError => e 478 | ex = e 479 | end 480 | 481 | expect(ex).to be_a(Abstriker::NotImplementedError) 482 | expect(ex.subclass).to eq(B19) 483 | expect(ex.abstract_method.owner).to eq(B1) 484 | expect(ex.abstract_method.name).to eq(:foo) 485 | end 486 | end 487 | 488 | context "module extend B1 and implement" do 489 | it "does not raise" do 490 | module B20 491 | extend B1 492 | 493 | def self.foo 494 | end 495 | end 496 | end 497 | end 498 | 499 | context "Module.new extend B1 and not implement" do 500 | it "raise Abstriker::NotImplementedError", aggregate_failures: true do 501 | ex = nil 502 | klass = nil 503 | begin 504 | Module.new do 505 | klass = self 506 | extend B1 507 | end 508 | rescue Abstriker::NotImplementedError => e 509 | ex = e 510 | end 511 | 512 | expect(ex).to be_a(Abstriker::NotImplementedError) 513 | expect(ex.subclass).to eq(klass) 514 | expect(ex.abstract_method.owner).to eq(B1) 515 | expect(ex.abstract_method.name).to eq(:foo) 516 | end 517 | end 518 | 519 | context "Module.new extend B1 and implement" do 520 | it "does not raise" do 521 | Module.new do 522 | def self.foo 523 | end 524 | 525 | extend B1 526 | end 527 | end 528 | end 529 | 530 | context "class << self include B1 and not implement" do 531 | it "raise Abstriker::NotImplementedError", aggregate_failures: true do 532 | ex = nil 533 | begin 534 | module B21 535 | class << self 536 | include B1 537 | end 538 | end 539 | rescue Abstriker::NotImplementedError => e 540 | ex = e 541 | end 542 | 543 | expect(ex).to be_a(Abstriker::NotImplementedError) 544 | expect(ex.subclass).to eq(B21.singleton_class) 545 | expect(ex.abstract_method.owner).to eq(B1) 546 | expect(ex.abstract_method.name).to eq(:foo) 547 | end 548 | end 549 | 550 | context "class << self include B1 and implement" do 551 | it "does not raise" do 552 | module B22 553 | class << self 554 | def foo 555 | end 556 | 557 | include B1 558 | end 559 | end 560 | end 561 | end 562 | end 563 | 564 | context "D1 class has abstract singleton method" do 565 | before do 566 | class D1 567 | extend Abstriker 568 | 569 | abstract_singleton_method def self.foo 570 | end 571 | end 572 | end 573 | 574 | context "subclass not implement" do 575 | it "raise Abstriker::NotImplementedError", aggregate_failures: true do 576 | ex = nil 577 | begin 578 | class D2 < D1 579 | end 580 | rescue Abstriker::NotImplementedError => e 581 | ex = e 582 | end 583 | 584 | expect(ex).to be_a(Abstriker::NotImplementedError) 585 | expect(ex.subclass).to eq(D2) 586 | expect(ex.abstract_method.owner).to eq(D1.singleton_class) 587 | expect(ex.abstract_method.name).to eq(:foo) 588 | end 589 | end 590 | 591 | context "subclass implement" do 592 | it "does not raise" do 593 | class D3 < D1 594 | def self.foo 595 | end 596 | end 597 | 598 | class D5 < D3 599 | end 600 | 601 | class D6 < D1 602 | class << self 603 | def foo 604 | end 605 | end 606 | end 607 | end 608 | end 609 | 610 | context "Class.new(D1) and implement" do 611 | it "does not raise" do 612 | Class.new(D1) do 613 | pr = proc do 614 | end 615 | pr.call 616 | 617 | [1, 2, 3].each do |n| 618 | n * 2 619 | end 620 | 621 | class << self 622 | def foo 623 | end 624 | end 625 | end 626 | end 627 | end 628 | 629 | context "subclass and raise exception" do 630 | it "does raise the exception" do 631 | ex = nil 632 | begin 633 | class D4 < D1 634 | class << self 635 | raise "err" 636 | end 637 | end 638 | rescue => e 639 | ex = e 640 | end 641 | 642 | expect(ex).to be_a(RuntimeError) 643 | end 644 | end 645 | 646 | context "Class.new(D1) and raise exception" do 647 | it "does raise the exception" do 648 | ex = nil 649 | begin 650 | Class.new(D1) do 651 | class << self 652 | raise "err" 653 | end 654 | end 655 | rescue => e 656 | ex = e 657 | end 658 | 659 | expect(ex).to be_a(RuntimeError) 660 | end 661 | end 662 | 663 | context "Class.new(D1) with no block" do 664 | it "raise Abstriker::NotImplementedError" do 665 | ex = nil 666 | begin 667 | Class.new(D1) 668 | rescue Abstriker::NotImplementedError => e 669 | ex = e 670 | end 671 | 672 | expect(ex).to be_a(Abstriker::NotImplementedError) 673 | end 674 | end 675 | 676 | context "subclass include refined module that has implementation" do 677 | module D8 678 | end 679 | 680 | using Module.new { 681 | refine D8 do 682 | def foo 683 | end 684 | end 685 | } 686 | 687 | it "does not raise" do 688 | ex = nil 689 | begin 690 | class D7 < D1 691 | extend D8 692 | end 693 | rescue Abstriker::NotImplementedError => e 694 | ex = e 695 | end 696 | 697 | expect(ex).to be_a(Abstriker::NotImplementedError) 698 | expect(ex.subclass).to eq(D7) 699 | expect(ex.abstract_method.name).to eq(:foo) 700 | expect(D7.foo).to be_nil 701 | end 702 | end 703 | end 704 | 705 | context "complex pattern" do 706 | it do 707 | expect { 708 | module Foo 709 | extend Abstriker 710 | 711 | abstract def foo 712 | end 713 | end 714 | 715 | module Bar 716 | extend Abstriker 717 | 718 | abstract def bar 719 | end 720 | end 721 | 722 | c = Class.new do 723 | extend Foo 724 | 725 | self.include Foo 726 | include Bar 727 | 728 | def foo 729 | end 730 | alias :bar :foo 731 | 732 | def self.foo 733 | end 734 | end 735 | 736 | class Hoge < c 737 | end 738 | 739 | Module.new do 740 | include \ 741 | Foo 742 | 743 | class_eval do 744 | extend Foo 745 | end 746 | 747 | def foo 748 | end 749 | 750 | def self.foo 751 | end 752 | end 753 | }.not_to raise_error 754 | 755 | ex = nil 756 | begin 757 | c = Class.new 758 | c.send(:include, Foo) 759 | rescue Abstriker::NotImplementedError => e 760 | ex = e 761 | end 762 | 763 | expect(ex).to be_a(Abstriker::NotImplementedError) 764 | expect(ex.abstract_method.name).to eq(:foo) 765 | 766 | ex = nil 767 | begin 768 | c = Class.new 769 | c.extend Foo 770 | rescue Abstriker::NotImplementedError => e 771 | ex = e 772 | end 773 | 774 | expect(ex).to be_a(Abstriker::NotImplementedError) 775 | expect(ex.abstract_method.name).to eq(:foo) 776 | end 777 | end 778 | 779 | end 780 | --------------------------------------------------------------------------------