├── lib ├── ruby-static-checker.rb └── ruby-static-checker │ └── extn.rb ├── examples └── foo.rb ├── ruby-static-checker.gemspec ├── LICENSE ├── README.markdown └── bin └── ruby-static-checker /lib/ruby-static-checker.rb: -------------------------------------------------------------------------------- 1 | $:.unshift(File.dirname(__FILE__)) 2 | 3 | module RubyStaticChecker 4 | end 5 | 6 | require 'ruby-static-checker/extn' 7 | -------------------------------------------------------------------------------- /examples/foo.rb: -------------------------------------------------------------------------------- 1 | # SAMPLE OUTPUT: 2 | # gdb@fire-hazard:~$ ruby-static-checker ./foo.rb 3 | # Possible name error while calling B.a 4 | # Possible name error while calling B.baz 5 | # Possible name error while calling B.a 6 | # Possible name error while calling A.c 7 | 8 | class A 9 | def foo 10 | a = 1 11 | a 12 | end 13 | 14 | def bar 15 | foo 16 | private_methods 17 | end 18 | 19 | def car 20 | c 21 | c = 1 22 | end 23 | end 24 | 25 | module B 26 | def foo 27 | a 28 | instance_bar 29 | end 30 | 31 | def instance_bar 32 | baz 33 | foo 34 | end 35 | 36 | def self.bar 37 | a 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /lib/ruby-static-checker/extn.rb: -------------------------------------------------------------------------------- 1 | module Kernel 2 | # from http://redcorundum.blogspot.com/2006/05/kernelqualifiedconstget.html 3 | def fetch_class(str) 4 | path = str.to_s.split('::') 5 | from_root = path[0].empty? 6 | if from_root 7 | from_root = [] 8 | path = path[1..-1] 9 | else 10 | start_ns = ((Class === self) || (Module === self)) ? self : self.class 11 | from_root = start_ns.to_s.split('::') 12 | end 13 | until from_root.empty? 14 | begin 15 | return (from_root+path).inject(Object) { |ns,name| ns.const_get(name) } 16 | rescue NameError 17 | from_root.delete_at(-1) 18 | end 19 | end 20 | path.inject(Object) { |ns,name| ns.const_get(name) } 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /ruby-static-checker.gemspec: -------------------------------------------------------------------------------- 1 | Gem::Specification.new do |s| 2 | s.name = 'ruby-static-checker' 3 | s.version = '0.0.1' 4 | s.authors = ["Greg Brockman"] 5 | s.email = ["gdb@gregbrockman.com"] 6 | s.homepage = 'https://github.com/gdb/ruby-static-checker' 7 | s.summary = %q{A static checker for Ruby} 8 | s.description = %q{Ruby-static-checker is, well, a static analysis tool for 9 | Ruby. Ruby-static-checker's goal is to provide a simple set of sanity 10 | checks (starting with name error detection) for existing codebases 11 | without requiring any additional programmer work. This means you 12 | should be able to, without modifying your application, run 13 | 14 | ruby-static-checker /path/to/mainfile.rb 15 | 16 | And have the static analysis Just Work.} 17 | 18 | s.rubyforge_project = "ruby-static-checker" 19 | 20 | s.files = `git ls-files`.split("\n") 21 | s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) } 22 | 23 | s.add_runtime_dependency 'ParseTree' 24 | end 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011 Greg Brockman, http://github.com/gdb/ruby-static-checker 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | Ruby-static-checker (a simple Ruby bugfinding tool) 2 | =================================================== 3 | 4 | It drives me crazy that my most common bug in Ruby is a name 5 | error. Maybe I typo a variable, or maybe I rename a class name in 6 | every location except for one, or maybe a cosmic ray flips a bit in my 7 | source before I commit. These are all mistakes that would be caught in 8 | a more static language like C or Java. 9 | 10 | As ruby-static-checker shows, there's no inherent reason we can't have 11 | a static checker to help catch these sorts of bugs. Because Ruby is so 12 | dynamic, it is impossible to write a checker with 100% accuracy 13 | (you'll always be able to write byzantine programs that do crazy 14 | method redefinitions at runtime). But given a reasonable codebase, you 15 | should be able to write a tool to find name errors with a reasonable 16 | amount of certainty. 17 | 18 | Ruby-static-checker is a static bugfinding tool for 19 | Ruby. Ruby-static-checker's goal is to provide a simple set of sanity 20 | checks (starting with name error detection) for existing codebases 21 | without requiring any additional programmer work. This means you 22 | should be able to, without modifying your application, run 23 | 24 | ```shell 25 | ruby-static-checker /path/to/mainfile.rb 26 | ``` 27 | 28 | And have the static analysis Just Work. 29 | 30 | Note that we don't actually completely achieve this 31 | goal. Ruby-static-checker does require your codebase to load all 32 | required code but not execute the program if loaded as a library (use 33 | something like "main if $0 == __FILE__" to get this property). You 34 | can't have everything. 35 | 36 | What does it do? 37 | ---------------- 38 | 39 | At the moment, ruby-static-checker just searches for name errors in 40 | your code. It's possible I'll make it do other things in the future. 41 | 42 | Ruby-static-checker can find bugs like in the following (contrived) example: 43 | 44 | ```ruby 45 | class A 46 | def self.foo 47 | baz = 1 48 | # Whoops, typod 'baz'! 49 | puts bas 50 | end 51 | end 52 | ``` 53 | 54 | The output of this should be (yes, I know, this output could be way better): 55 | 56 | ```shell 57 | gdb@fire-hazard:~$ ruby-static-checker /path/to/a.rb 58 | Possible name error while calling A.bas 59 | ``` 60 | 61 | The current implementation is very barebones. It does have some false 62 | positives (and given the dynamic nature of Ruby, there's basically 63 | always going to be some false rate in either direction) but I've 64 | already used it to find real bugs. 65 | 66 | How does it work? 67 | ----------------- 68 | 69 | Magic! 70 | 71 | Well, really, how does it work? 72 | ------------------------------- 73 | 74 | Ruby-static-checker requires the path you give it, thus loading its 75 | code into memory. Any load-time class and method generation and any 76 | requires are carried out. We then iterate over all newly created 77 | classes, analyzing the ASTs of their methods (using the most excellent 78 | ParseTree library). 79 | 80 | This has the benefit that we don't need to statically determine what 81 | the requires graph will look like. Obviously people can always require 82 | more files at runtime, but that's relatively rare so we don't try to 83 | address that case. 84 | 85 | Are there any other static analysis tools for Ruby out there? 86 | ------------------------------------------------------------- 87 | 88 | Yes. Some of the cooler ones are druby, reek, and roodi. 89 | 90 | Limitations 91 | ----------- 92 | 93 | Tested on Ruby 1.8. I added handling of AST nodes as I encountered 94 | them in actual code, so it's likely there are some more esoteric nodes 95 | that I missed. 96 | 97 | I'd love to contribute. Are patches welcome? 98 | -------------------------------------------- 99 | 100 | Glad to hear it! Patches are indeed most welcome. Just open a pull 101 | request on Github. 102 | -------------------------------------------------------------------------------- /bin/ruby-static-checker: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # 3 | # Detect name errors in Ruby programs 4 | # 5 | # Works by first loading the Ruby code, then analyzing that. 6 | # 7 | # Obviously ruby-checker can be defeated through use of 8 | # method_missing, dynamic method definition, and other magical 9 | # magic. So don't use it on code using those paradigms. 10 | # 11 | # Architecture: 12 | # 13 | # - Load the code 14 | # - Make a pass through all classes to build a mapping of class names 15 | # to environments 16 | # - Make a pass through all classes, checking each's calls to functions 17 | # against the environments built in the first stage. 18 | # 19 | # Notes: how does ParseTree deal with extends? 20 | require 'logger' 21 | require 'optparse' 22 | require 'pp' 23 | require 'set' 24 | 25 | require 'rubygems' 26 | require 'parse_tree' 27 | 28 | require File.join(File.dirname(__FILE__), '../lib/ruby-static-checker') 29 | 30 | $log = Logger.new(STDOUT) 31 | $log.level = Logger::WARN 32 | 33 | module RubyStaticChecker 34 | class Error < StandardError; end 35 | class TreeError < StandardError; end 36 | 37 | class Env 38 | attr_reader :me 39 | @@envs = {} 40 | 41 | def self.get_and_populate(klass) 42 | raise Error.new("Invalid klass: #{klass.inspect}") unless klass && (klass.kind_of?(Class) || klass.kind_of?(Module)) 43 | env = get(klass) 44 | Analyzer.build_environment_for_class(klass, env) unless env.populated? 45 | env 46 | end 47 | 48 | def self.get(klass) 49 | @@envs[klass] ||= self.new(klass) 50 | end 51 | 52 | def initialize(me) 53 | @me = me 54 | @instance = {} 55 | @class = {} 56 | @superclass = nil 57 | @mixins = [] 58 | end 59 | 60 | def mark_populated!; @populated = true; end 61 | def populated?; @populated; end 62 | 63 | def add_instance_method(name) 64 | @instance[name] = :method 65 | end 66 | def instance_method?(name) 67 | !!@instance[name] 68 | end 69 | 70 | def add_class_method(name) 71 | @class[name] = :method 72 | end 73 | def class_method?(name) 74 | !!@class[name] 75 | end 76 | 77 | def set_superclass(name) 78 | @superclass = name 79 | end 80 | 81 | def add_mixin(name) 82 | @mixins << name 83 | end 84 | 85 | def defined?(var, klass_context) 86 | # TODO: watch out for method_missing 87 | return true if builtins.include?(var) 88 | return true if klass_context && builtin_class_methods.include?(var) 89 | 90 | if klass_context 91 | return true if @class[var] 92 | delegators = [@superclass] 93 | else 94 | return true if @instance[var] 95 | delegators = [@superclass] + @mixins 96 | end 97 | 98 | delegators.compact.any? do |parent| 99 | self.class.get_and_populate(parent).defined?(var, klass_context) 100 | end 101 | end 102 | 103 | def pretty_call(name, klass_context) 104 | if klass_context 105 | "#{@me}.#{name}" 106 | else 107 | "#{@me}.#{name}" 108 | end 109 | end 110 | 111 | private 112 | 113 | def builtins 114 | Set.new([:raise, :Integer, :Array, :warn, :block_given?, :lambda, 115 | :print, :puts, :sprintf, :eval, :caller, :select, :extend, 116 | :include, :require, :loop, :format, :sleep, :catch, :throw, 117 | :exec, :fork, :dup, :respond_to?, :object_id, :p]) 118 | end 119 | 120 | def builtin_class_methods 121 | # TODO: this includes some private methods, maybe should distinguish 122 | Set.new([:attr_writer, :attr_reader, :attr_accessor, :const_set, :define_method, 123 | :remove_const, :remove_method]) 124 | end 125 | end 126 | 127 | class Main 128 | attr_reader :global_env 129 | 130 | def initialize(path) 131 | @path = path 132 | @builtin_classes = all_classes_to_analyze 133 | end 134 | 135 | def main 136 | load_code 137 | # build_environments 138 | check_against_environments 139 | end 140 | 141 | def load_code 142 | $log.debug("Loading #{@path}") 143 | require @path 144 | end 145 | 146 | # TODO: write this with some cooler pattern-matching 147 | def build_environments 148 | classes_to_analyze.each { |klass| Analyzer.analyze_class(klass) } 149 | end 150 | 151 | def check_against_environments 152 | classes_to_analyze.each do |klass| 153 | begin 154 | Checker.check_for_name_errors(klass) 155 | rescue 156 | $log.warn("Skipping #{klass.inspect}") 157 | end 158 | end 159 | end 160 | 161 | def classes_to_analyze 162 | all_classes_to_analyze - @builtin_classes 163 | end 164 | 165 | def all_classes_to_analyze 166 | ObjectSpace.each_object.select do |object| 167 | object.kind_of?(Class) || object.kind_of?(Module) 168 | end 169 | end 170 | end 171 | 172 | module Util 173 | def expect(val1, val2=nil, &blk) 174 | if block_given? 175 | raise Error.new("Cannot call expect with a block and a second argument") unless val2.nil? 176 | raise TreeError.new("Expected #{val1.inspect} did not pass the provided block") unless blk.call(val1) 177 | else 178 | raise TreeError.new("Expected #{val1.inspect} but #{val2.inspect} obtained") unless val1 == val2 179 | end 180 | end 181 | 182 | def symbolize(name) 183 | name == '' ? :nil : name.to_sym 184 | end 185 | 186 | def all_methods(klass) 187 | klass.methods + klass.protected_methods + klass.private_methods 188 | end 189 | 190 | def all_instance_methods(klass) 191 | klass.instance_methods + klass.protected_instance_methods + klass.private_instance_methods 192 | end 193 | end 194 | 195 | module Analyzer 196 | extend Util 197 | 198 | def self.analyze_class(klass) 199 | env = Env.get(klass) 200 | build_environment_for_class(klass, env) 201 | end 202 | 203 | def self.build_environment_for_class(klass, env) 204 | # TODO: should probably just do this for everything 205 | instance_methods = ['initialize'] + all_instance_methods(klass) 206 | instance_methods.each { |name| env.add_instance_method(name.to_sym) } 207 | all_methods(klass).each { |name| env.add_class_method(name.to_sym) } 208 | 209 | return if klass == Object 210 | 211 | begin 212 | ast = ParseTree.translate(klass) 213 | rescue 214 | $log.error("Could not build AST for #{klass.inspect}") 215 | raise 216 | end 217 | $log.debug("Building enviroment for #{klass.inspect}: #{ast.pretty_inspect}") 218 | 219 | # Should really be doing this functionally. Oh well. 220 | case type = ast.shift 221 | when :class 222 | # Apparently some classes (e.g. Tempfile's superclass) can 223 | # have empty string names. We are currently using a name-based 224 | # system; may want to switch to object-based in the future. 225 | expect(ast.shift) do |value| 226 | if klass.name.length > 0 227 | klass.name.to_sym == value 228 | else 229 | value == :nil || value.to_s =~ /^UnnamedClass_\d+$/ 230 | end 231 | end 232 | # Can't ParseTree.translate(Object) so this works. This may 233 | # also need the UnnamedClass logic. 234 | expect([:const, symbolize(klass.superclass.name)], ast.shift) 235 | env.set_superclass(klass.superclass) 236 | when :module 237 | expect(klass.name.to_sym, ast.shift) 238 | else 239 | raise NotImplementedError.new("Unrecognized class/module type #{type.inspect}") 240 | end 241 | ast.each { |method| add_to_environment(method, env) } 242 | 243 | env.mark_populated! 244 | end 245 | 246 | def self.add_to_environment(ast, env) 247 | case type = ast.shift 248 | when :defn 249 | name = ast.shift 250 | unless env.instance_method?(name) 251 | $log.info("Definition for #{env.pretty_call(name, false)} found, but it is not listend as an instance method") 252 | env.add_instance_method(name) 253 | end 254 | ast.shift 255 | when :defs 256 | expect([:self], ast.shift) 257 | name = ast.shift 258 | unless env.class_method?(name) 259 | $log.info("Definition for #{env.pretty_call(name, true)} found, but it is not listend as a class method") 260 | env.add_class_method(name) 261 | end 262 | ast.shift 263 | when :call 264 | # TODO: clean this up a lot 265 | expect(nil, ast.shift) 266 | expect(:include, ast.shift) 267 | arglist = ast.shift 268 | expect(:arglist, arglist.shift) 269 | mixin_name = get_nested_name(arglist.shift) 270 | mixin = fetch_class(mixin_name) 271 | expect([], arglist) 272 | env.add_mixin(mixin) 273 | else 274 | raise NotImplementedError.new("Unrecognized method definition type #{type.inspect}") 275 | end 276 | 277 | expect([], ast) 278 | end 279 | 280 | def self.get_nested_name(ast) 281 | case type = ast.shift 282 | when :colon2 283 | namespace = get_nested_name(ast.shift) 284 | name = ast.shift 285 | res = "#{namespace}::#{name}".to_sym 286 | when :const 287 | res = ast.shift 288 | else 289 | raise NotImplementedError.new("Unrecognized class nesting construct #{type.inspect}") 290 | end 291 | 292 | expect([], ast) 293 | res 294 | end 295 | end 296 | 297 | module Checker 298 | extend Util 299 | 300 | def self.check_for_name_errors(klass) 301 | env = Env.get_and_populate(klass) 302 | check_class_against_environment(klass, env) 303 | end 304 | 305 | def self.check_class_against_environment(klass, env) 306 | ast = ParseTree.translate(klass) 307 | $log.debug("Checking for name errors for #{klass.inspect}: #{ast.pretty_inspect}") 308 | 309 | case type = ast.shift 310 | when :class 311 | expect(ast.shift) do |value| 312 | if klass.name.length > 0 313 | klass.name.to_sym == value 314 | else 315 | value == :nil || value.to_s =~ /^UnnamedClass_\d+$/ 316 | end 317 | end 318 | expect([:const, symbolize(klass.superclass.name)], ast.shift) 319 | when :module 320 | expect(klass.name.to_sym, ast.shift) 321 | else 322 | raise NotImplementedError.new("Unrecognized class/module type #{type.inspect}") 323 | end 324 | ast.each { |method| check_method_against_environment(method, env) } 325 | 326 | env.mark_populated! 327 | end 328 | 329 | def self.check_method_against_environment(ast, env) 330 | $log.debug("Checking method #{ast.inspect}") 331 | 332 | case type = ast.shift 333 | when :defn 334 | name = ast.shift 335 | check_method_body(ast.shift, false, env) 336 | when :defs 337 | expect([:self], ast.shift) 338 | name = ast.shift 339 | check_method_body(ast.shift, true, env) 340 | when :call 341 | # Who cares about mixins 342 | ast = [] 343 | else 344 | raise NotImplementedError.new("Unrecognized method definition type #{type.inspect}") 345 | end 346 | 347 | expect([], ast) 348 | end 349 | 350 | def self.check_method_body(tree, klass_context, env) 351 | case type = tree.shift 352 | when :and, :or, :op_asgn_and, :op_asgn_or 353 | # Not sure how :op_asgn_or differs from :or 354 | # >> class A; def foo; a && b && c; end; end 355 | # => nil 356 | # >> ParseTree.translate A, :foo 357 | # => [:defn, :foo, [:scope, [:block, [:args], [:and, [:vcall, :a], [:and, [:vcall, :b], [:vcall, :c]]]]]] 358 | a = tree.shift 359 | b = tree.shift 360 | check_method_body(a, klass_context, env) 361 | check_method_body(b, klass_context, env) 362 | when :args 363 | # No idea what this is 364 | # [:defs, [:self], :incompatible_argument_styles, [:scope, [:args, :*]]] 365 | expect(:*, tree.shift) 366 | when :argscat, :op_asgn1, :op_asgn2 367 | # No idea what this is 368 | while tree.length > 0 369 | case elt = tree.shift 370 | when Symbol: next 371 | when Array: check_method_body(elt, klass_context, env) 372 | else 373 | raise "Unrecognized elt #{elt.inspect}" 374 | end 375 | end 376 | when :alias 377 | # TODO: make sure target actually is defined if it's a lit 378 | # >> class A; def foo; alias :a :b; end; end 379 | # => nil 380 | # >> ParseTree.translate A, :foo 381 | # => [:defn, :foo, [:scope, [:block, [:args], [:alias, [:lit, :a], [:lit, :b]]]]] 382 | source = tree.shift 383 | target = tree.shift 384 | check_method_body(source, klass_context, env) 385 | check_method_body(target, klass_context, env) 386 | when :array, :hash 387 | check_method_body(tree.shift, klass_context, env) while tree.length > 0 388 | when :attrset 389 | # Not really sure what this one is, but ok 390 | # [:defn, :datetime_format=, [:attrset, :@datetime_format]] 391 | expect(tree.shift) { |value| value.kind_of?(Symbol) && value.to_s.start_with?('@') } 392 | when :back_ref 393 | # Not sure what this is 394 | expect(tree.shift) { |value| value.kind_of?(Symbol) } 395 | when :block 396 | # Huge hack: 397 | # >> module A; def foo(a,b,c=1); end; end 398 | # => nil 399 | # >> ParseTree.translate A 400 | # => [:module, :A, [:defn, :foo, [:scope, [:block, [:args, :a, :b, :c, [:block, [:lasgn, :c, [:lit, 1]]]], [:nil]]]]] 401 | tree.shift if (args = tree[0]) && (args[0] == :args) 402 | check_method_body(tree.shift, klass_context, env) while tree.length > 0 403 | when :block_arg 404 | # TODO: add an expect? Do something useful with types? 405 | # [:defn, :log, [:scope, [:block, [:args, :severity, :message, [:block, [:lasgn, :message, [:nil]]]], [:block_arg, :block], [:if, [:ivar, :@log], [:block_pass, [:lvar, :block], [:call, [:ivar, :@log], :add, [:array, [:lvar, :severity], [:lvar, :message], [:ivar, :@appname]]]], nil]]]] 406 | tree.shift 407 | when :block_pass 408 | # >> class A; def foo; bar(&blk); end; end 409 | # => nil 410 | # >> ParseTree.translate A 411 | # => [:class, :A, [:const, :Object], [:defn, :foo, [:scope, [:block, [:args], [:block_pass, [:vcall, :blk], [:fcall, :bar]]]]]] 412 | block = tree.shift 413 | fcall = tree.shift 414 | check_method_body(block, klass_context, env) 415 | check_method_body(fcall, klass_context, env) 416 | when :bmethod, :dot2, :dot3 417 | # No idea what there are 418 | while tree.length > 0 419 | component = tree.shift 420 | check_method_body(component, klass_context, env) if component 421 | end 422 | when :call, :attrasgn 423 | # [:call, [:gvar, :$!] 424 | # [[:const, :Logger], :new, [:array, [:const, :STDERR]]] 425 | $log.debug('TODO: look for undefined method calls') 426 | obj = tree.shift 427 | method = tree.shift 428 | # arglist 429 | tree = [] 430 | when :case 431 | # >> class A; def foo; case 1; when 2: 3; else; 4; end; end; end 432 | # => nil 433 | # >> ParseTree.translate A, :foo 434 | # => [:defn, :foo, [:scope, [:block, [:args], [:case, [:lit, 1], [:when, [:array, [:lit, 2]], [:lit, 3]], [:lit, 4]]]]] 435 | value = tree.shift 436 | check_method_body(value, klass_context, env) if value 437 | while tree.length > 0 438 | when_spec = tree.shift 439 | # As long as it's not the else statement 440 | expect(:when, when_spec[0]) if tree.length > 0 441 | check_method_body(when_spec, klass_context, env) if when_spec 442 | end 443 | when :cfunc 444 | # Call a C function, I think 445 | addr = tree.shift 446 | arg = tree.shift 447 | expect(addr) { |value| value.kind_of?(Numeric) } 448 | expect(arg) { |value| value.kind_of?(Numeric) } 449 | when :colon2 450 | $log.debug('TODO: look for defined nested constants') 451 | nested = tree.shift 452 | namespace = tree.shift 453 | check_method_body(nested, klass_context, env) 454 | when :colon3 455 | # >> class A; def foo; ::String; end; end 456 | # => nil 457 | # >> ParseTree.translate A, :foo 458 | # => [:defn, :foo, [:scope, [:block, [:args], [:colon3, :String]]]] 459 | $log.debug('TODO: look for defined toplevel constants') 460 | tree.shift 461 | when :const 462 | $log.debug('TODO: look for defined constants') 463 | tree.shift 464 | when :cvar 465 | # Class variable 466 | expect(tree.shift) { |value| value.kind_of?(Symbol) && value.to_s.start_with?('@@') } 467 | when :cvasgn 468 | # Class variable assign 469 | name = tree.shift 470 | target = tree.shift 471 | expect(name) { |value| value.kind_of?(Symbol) && value.to_s.start_with?('@@') } 472 | check_method_body(target, klass_context, env) while tree.length > 0 473 | when :dasgn, :dasgn_curr, :dvar 474 | # Not really sure what these are 475 | # [:defn, :find, [:scope, [:block, [:args, :glob], [:iter, [:call, [:ivar, :@gemspecs], :find], [:dasgn_curr, :spec], [:fcall, :matching_file?, [:array, [:dvar, :spec], [:lvar, :glob]]]]]]] 476 | expect(tree.shift) { |value| value.kind_of?(Symbol) } 477 | check_method_body(tree.shift, klass_context, env) while tree.length > 0 478 | when :defined, :not 479 | value = tree.shift 480 | check_method_body(value, klass_context, env) 481 | when :defn 482 | $log.warn('Dynamically defined method encountered') 483 | name = tree.shift 484 | body = tree.shift 485 | check_method_body(body, klass_context, env) 486 | when :defs 487 | target = tree.shift 488 | name = tree.shift 489 | body = tree.shift 490 | check_method_body(target, true, env) 491 | # Don't check the body because it's in some other random 492 | # object's scope 493 | when :dregx, :dregx_once 494 | # Not really sure what an integer here means 495 | # [:dregx, "^", [:evstr, [:lvar, :gem_pattern]], 1] 496 | while tree.length > 0 497 | case string_component = tree.shift 498 | when String: next 499 | when Numeric: next 500 | when Array: check_method_body(string_component, klass_context, env) 501 | else 502 | raise "Unrecognized type of string component #{string_component.inspect}" 503 | end 504 | end 505 | when :dstr, :dsym, :dxstr 506 | # Not sure what diff between dxstr and dstr is 507 | # [:dstr, "Start of ", [:evstr, [:ivar, :@appname]], [:str, "."]]]] 508 | # [:dxstr, "", [:evstr, [:lvar, :cmd]]] 509 | while tree.length > 0 510 | case string_component = tree.shift 511 | when String: next 512 | when Array: check_method_body(string_component, klass_context, env) 513 | else 514 | raise "Unrecognized type of string component #{string_component.inspect}" 515 | end 516 | end 517 | when :ensure 518 | # >> class A; def foo; begin; 1; ensure 2; end; end; end 519 | # => nil 520 | # >> ParseTree.translate A 521 | # => [:class, :A, [:const, :Object], [:defn, :foo, [:scope, [:block, [:args], [:ensure, [:lit, 1], [:lit, 2]]]]]] 522 | begin_block = tree.shift 523 | ensure_block = tree.shift 524 | check_method_body(begin_block, klass_context, env) 525 | check_method_body(ensure_block, klass_context, env) 526 | when :evstr 527 | # >> class A; def foo; "#{a + b}"; end; end 528 | # => nil 529 | # >> ParseTree.translate A 530 | # => [:class, :A, [:const, :Object], [:defn, :foo, [:scope, [:block, [:args], [:dstr, "", [:evstr, [:call, [:vcall, :a], :+, [:array, [:vcall, :b]]]]]]]]] 531 | # TODO: might have multiple possible args 532 | arg = tree.shift 533 | check_method_body(arg, klass_context, env) 534 | when :false, :nil, :true 535 | when :fbody 536 | # Not really sure what this is 537 | body = tree.shift 538 | check_method_body(body, klass_context, env) 539 | when :for 540 | # >> class A; def foo; for i in j; 2; end; end; end 541 | # => nil 542 | # >> ParseTree.translate A, :foo 543 | # => [:defn, :foo, [:scope, [:block, [:args], [:for, [:vcall, :j], [:lasgn, :i], [:lit, 2]]]]] 544 | value = tree.shift 545 | holder = tree.shift 546 | body = tree.shift 547 | check_method_body(value, klass_context, env) 548 | check_method_body(holder, klass_context, env) if holder 549 | check_method_body(body, klass_context, env) if body 550 | when :iter 551 | # >> class A; def foo; bar { puts }; end; end; end 552 | # => nil 553 | # >> ParseTree.translate A, :foo 554 | # => [:defn, :foo, [:scope, [:block, [:args], [:iter, [:fcall, :bar], nil, [:vcall, :puts]]]]] 555 | value = tree.shift 556 | holder = tree.shift 557 | body = tree.shift 558 | check_method_body(value, klass_context, env) 559 | check_method_body(holder, klass_context, env) if holder 560 | # TODO: do something with this block, maybe. Can't really 561 | # recurse because who knows where this block will 562 | when :gasgn 563 | # Global assign 564 | name = tree.shift 565 | assigned = tree.shift 566 | expect(name) { |value| value.kind_of?(Symbol) && value.to_s.start_with?('$') } 567 | # Not sure what assigned being nil means, but ok. 568 | check_method_body(assigned, klass_context, env) if assigned 569 | when :gvar 570 | # Global variable 571 | name = tree.shift 572 | expect(name) { |value| value.kind_of?(Symbol) && value.to_s.start_with?('$') } 573 | when :if 574 | predicate = tree.shift 575 | positive = tree.shift 576 | negative = tree.shift 577 | check_method_body(predicate, klass_context, env) 578 | check_method_body(positive, klass_context, env) if positive 579 | check_method_body(negative, klass_context, env) if negative 580 | when :ivar, :lit, :lvar 581 | # TODO: add an expect 582 | tree.shift 583 | when :lasgn, :iasgn 584 | var = tree.shift 585 | value = tree.shift 586 | check_method_body(value, klass_context, env) if value 587 | when :masgn 588 | # Not reaaally sure what this is. Probably something with 589 | # pattern-matching. 590 | a = tree.shift 591 | b = tree.shift 592 | c = tree.shift 593 | check_method_body(a, klass_context, env) if a 594 | check_method_body(b, klass_context, env) if b 595 | check_method_body(c, klass_context, env) if c 596 | when :match2, :match3 597 | # What is the difference between match2 and match3? Who knows. 598 | regex = tree.shift 599 | value = tree.shift 600 | check_method_body(regex, klass_context, env) 601 | check_method_body(value, klass_context, env) 602 | when :nth_ref 603 | # No idea what this is 604 | expect(tree.shift) { |value| value.kind_of?(Integer) } 605 | when :resbody 606 | # >> class A; def foo; begin 1; rescue Exception => e; 2; end; end; end 607 | # => nil 608 | # >> ParseTree.translate A, :foo 609 | # => [:defn, :foo, [:scope, [:block, [:args], [:rescue, [:lit, 1], [:resbody, [:array, [:const, :Exception]], [:block, [:lasgn, :e, [:gvar, :$!]], [:lit, 2]]]]]]] 610 | exception_type = tree.shift 611 | rescue_body = tree.shift 612 | # Not sure what this is... else of some sort? 613 | other_body = tree.shift 614 | check_method_body(exception_type, klass_context, env) if exception_type 615 | check_method_body(rescue_body, klass_context, env) if rescue_body 616 | check_method_body(other_body, klass_context, env) if other_body 617 | when :rescue 618 | # >> class A; def foo; begin; 1; rescue 2; end; end; end 619 | # => nil 620 | # >> ParseTree.translate A 621 | # => [:class, :A, [:const, :Object], [:defn, :foo, [:scope, [:block, [:args], [:rescue, [:lit, 1], [:resbody, [:array, [:lit, 2]]]]]]]] 622 | begin_block = tree.shift 623 | rescue_block = tree.shift 624 | else_block = tree.shift 625 | expect(:resbody, rescue_block[0]) 626 | check_method_body(begin_block, klass_context, env) 627 | check_method_body(rescue_block, klass_context, env) 628 | check_method_body(else_block, klass_context, env) if else_block 629 | when :retry 630 | when :return 631 | value = tree.shift 632 | check_method_body(value, klass_context, env) if value 633 | when :sclass 634 | # >> class A; def foo; class << self; end; end; end 635 | # => nil 636 | # >> ParseTree.translate A, :foo 637 | # => [:defn, :foo, [:scope, [:block, [:args], [:sclass, [:self], [:scope]]]]] 638 | name = tree.shift 639 | body = tree.shift 640 | check_method_body(name, klass_context, env) 641 | # body is some other environment. TODO: could do some more work here 642 | when :scope 643 | check_method_body(tree.shift, klass_context, env) 644 | when :self, :zarray 645 | # >> class A; def foo; []; end; end 646 | # => nil 647 | # >> ParseTree.translate A, :foo 648 | # => [:defn, :foo, [:scope, [:block, [:args], [:zarray]]]] 649 | when :splat 650 | check_method_body(tree.shift, klass_context, env) while tree.length > 0 651 | when :str 652 | str = tree.shift 653 | expect(true, str.kind_of?(String)) 654 | when :super, :yield, :next, :break 655 | # >> class A; def foo; super(a, b); end; end 656 | # => nil 657 | # >> ParseTree.translate A, :foo 658 | # => [:defn, :foo, [:scope, [:block, [:args], [:super, [:array, [:vcall, :a], [:vcall, :b]]]]]] 659 | arg = tree.shift 660 | # I've observed this with yield... not sure what it is 661 | second_arg = tree.shift 662 | check_method_body(arg, klass_context, env) if arg 663 | expect(second_arg) { |value| value.nil? || value == true } 664 | when :to_ary 665 | arg = tree.shift 666 | check_method_body(arg, klass_context, env) 667 | when :vcall, :fcall 668 | vcall_args = tree 669 | name = vcall_args.shift 670 | if env.defined?(name, klass_context) 671 | $log.info "Calling defined method #{env.pretty_call(name, klass_context)}" 672 | else 673 | puts "Possible name error while calling #{env.pretty_call(name, klass_context)}" 674 | end 675 | check_method_body(vcall_args.shift, klass_context, env) while vcall_args.length > 0 676 | when :when 677 | comparison = tree.shift 678 | body = tree.shift 679 | check_method_body(comparison, klass_context, env) 680 | check_method_body(body, klass_context, env) if body 681 | when :while, :until 682 | # >> class A; def foo; while 1; 2; end; end; end 683 | # => nil 684 | # >> ParseTree.translate A, :foo 685 | # => [:defn, :foo, [:scope, [:block, [:args], [:while, [:lit, 1], [:lit, 2], true]]]] 686 | condition = tree.shift 687 | body = tree.shift 688 | # TODO: figure out what this is actually used for 689 | expect(tree.shift) { |value| value == true || value == false } 690 | check_method_body(condition, klass_context, env) 691 | check_method_body(body, klass_context, env) if body 692 | when :zsuper 693 | # Dunno what this is 694 | else 695 | raise NotImplementedError.new("Unrecognized directive #{type.inspect} (remainder of tree is #{tree.inspect}") 696 | end 697 | 698 | expect([], tree) 699 | end 700 | end 701 | end 702 | 703 | def main 704 | options = {} 705 | optparse = OptionParser.new do |opts| 706 | opts.banner = "Usage: #{$0} [options] path/to/code" 707 | 708 | opts.on('-v', '--verbosity', 'Verbosity of debugging output') do 709 | $log.level -= 1 710 | end 711 | 712 | opts.on('-h', '--help', 'Display this message') do 713 | puts opts 714 | exit(1) 715 | end 716 | end 717 | optparse.parse! 718 | 719 | if ARGV.length != 1 720 | puts optparse 721 | return 1 722 | end 723 | 724 | runner = RubyStaticChecker::Main.new(*ARGV) 725 | runner.main 726 | return 0 727 | end 728 | 729 | ret = main 730 | begin 731 | exit(ret) 732 | rescue TypeError 733 | exit(0) 734 | end 735 | --------------------------------------------------------------------------------