ruby_scope 0.1.1

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.
@@ -0,0 +1,45 @@
1
+ RubyScope
2
+ ---------
3
+ Ruby colored binoculars for your code.
4
+
5
+ Usage: ruby_scope [options] path
6
+
7
+ Queries:
8
+ --def NAME Find the definition of instance method NAME
9
+ --class-def NAME Find the definition of class method NAME
10
+ --call NAME Find method calls of NAME
11
+ --class NAME Find definition of NAME
12
+ --variable NAME Find references to variable NAME
13
+ --assign NAME Find assignments to NAME
14
+ --any NAME Find any reference to NAME (class, variable, number)
15
+ --custom SEXP_PATH Searches for a custom SexpPath
16
+
17
+ Options:
18
+ -R Recursively search folders
19
+ --no-cache Do not use a cache
20
+ --cache PATH Use the cache at PATH (defaults to current dir)
21
+ -v, --verbose Verbose output
22
+ -h, --help Show this message
23
+
24
+ Find all the places `run` or `save` are called in your secret project:
25
+
26
+ ruby_scope -R --method 'run' --method 'save' ~/SecretProject
27
+
28
+ Where do I assign values to `a`:
29
+
30
+ ruby_scope -R --assign 'a' ~/SecretProject
31
+
32
+ Of course regular expressions are fair game:
33
+
34
+ ruby_scope -R --def '/^test/' ~/SecretProject
35
+
36
+ Wicked hacker? Go crazy and write your own SexpPath queries:
37
+
38
+ ruby_scope --custom 's(:call, s(:ivar, atom), :save, _)'
39
+
40
+ That finds all the saves on instance variables by the way.
41
+
42
+ Depends on RubyParser, SexpProcessor, and SexpPath, no gem yet.
43
+
44
+ Adam Sanderson
45
+ netghost@gmail.com
@@ -0,0 +1,52 @@
1
+ require 'rake'
2
+ require 'rake/testtask'
3
+ require 'rake/rdoctask'
4
+
5
+ begin
6
+ require 'jeweler'
7
+
8
+ Jeweler::Tasks.new do |s|
9
+ s.name = "ruby_scope"
10
+ s.summary = "Ruby colored binoluars for your code."
11
+ s.description = <<-DESC
12
+ A ruby hacker's search tool. Quickly interrogate your code, seek out
13
+ classes, methods, variable assignments, and more.
14
+ DESC
15
+ s.email = "netghost@gmail.com"
16
+ s.homepage = "http://github.com/adamsanderson/ruby_scope"
17
+ s.authors = ["Adam Sanderson"]
18
+ s.files = FileList["[A-Z]*", "{bin,lib,test}/**/*"]
19
+
20
+ s.add_dependency 'sexp_processor', '~> 3.0'
21
+ s.add_dependency 'ruby_parser', '~> 2.0'
22
+ s.add_dependency 'sexp_path', '>= 0.4'
23
+
24
+ # Testing
25
+ s.test_files = FileList["test/**/*_test.rb"]
26
+ s.add_development_dependency 'mocha', '>= 0.9.8'
27
+ end
28
+
29
+ rescue LoadError
30
+ puts "Jeweler not available. Install it for jeweler-related tasks with: sudo gem install jeweler"
31
+ end
32
+
33
+ Rake::RDocTask.new do |t|
34
+ #t.main = "README.rdoc"
35
+ t.rdoc_files.include("lib/**/*.rb")
36
+ end
37
+
38
+ Rake::TestTask.new do |t|
39
+ t.test_files = FileList['test/**/*_test.rb']
40
+ t.verbose = false
41
+ end
42
+
43
+ namespace :test do
44
+ ['unit', 'integration'].each do |type|
45
+ Rake::TestTask.new(type) do |t|
46
+ t.test_files = FileList["test/#{type}/**/*_test.rb"]
47
+ t.verbose = false
48
+ end
49
+ end
50
+ end
51
+
52
+ task :default => :test
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.1.1
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require File.dirname(__FILE__) + '/../lib/ruby_scope'
4
+ cli = RubyScope::CLI.new(ARGV)
5
+ cli.run
@@ -0,0 +1,20 @@
1
+ require 'rubygems'
2
+ require 'ruby_parser'
3
+ require 'sexp_path'
4
+ require 'unified_ruby'
5
+ require 'optparse'
6
+
7
+ # RubyScope is a command line tool designed to help you scan ruby code lexically.
8
+ # See the README for more information.
9
+ module RubyScope
10
+ end
11
+
12
+ # Load RubyScope
13
+ root = File.dirname(__FILE__)+'/ruby_scope/'
14
+ %w[
15
+ scanner
16
+ sexp_cache
17
+ cli
18
+ ].each do |file|
19
+ require root+file
20
+ end
@@ -0,0 +1,115 @@
1
+ class RubyScope::CLI
2
+ attr_reader :paths
3
+ attr_reader :cache_path
4
+ attr_reader :scanner
5
+
6
+ def initialize(args)
7
+ args = args.clone
8
+ @scanner = RubyScope::Scanner.new
9
+ @cache_path = FileUtils.pwd
10
+
11
+ opts = OptionParser.new do |opts|
12
+ opts.banner = "Usage: ruby_scope [options] queries path"
13
+
14
+ opts.separator ""
15
+ opts.separator "Queries:"
16
+
17
+ opts.on("--def NAME", "Find the definition of instance method NAME") do |name|
18
+ @scanner.add_query("s(:defn, #{v name}, _, _)")
19
+ end
20
+
21
+ opts.on("--class-def NAME", "Find the definition of class method NAME") do |name|
22
+ @scanner.add_query("s(:defs, _, #{v name}, _, _)")
23
+ end
24
+
25
+ opts.on("--call NAME", "Find method calls of NAME") do |name|
26
+ @scanner.add_query("s(:call, _, #{v name}, _)")
27
+ end
28
+
29
+ opts.on("--class NAME", "Find definition of NAME") do |name|
30
+ @scanner.add_query("s(:class, #{v name}, _, _)")
31
+ end
32
+
33
+ opts.on("--variable NAME", "Find references to variable NAME") do |name|
34
+ tag = instance_variable?(name) ? 'ivar' : 'lvar'
35
+ @scanner.add_query("s(:#{tag}, #{v name})")
36
+ end
37
+
38
+ # Finds block arguments, variable assignments, method arguments (in that order)
39
+ opts.on("--assign NAME", "Find assignments to NAME") do |name|
40
+ tag = instance_variable?(name) ? 'iasgn' : 'lasgn'
41
+ @scanner.add_query("s(:#{tag}, #{v name}) | s(:#{tag}, #{v name}, _) | (t(:args) & SexpPath::Matcher::Block.new{|s| s[1..-1].any?{|a| a == #{v name}}} )")
42
+ end
43
+
44
+ opts.on("--any NAME", "Find any reference to NAME (class, variable, number)") do |name|
45
+ @scanner.add_query("include(#{v name})")
46
+ end
47
+
48
+ opts.on("--custom SEXP_PATH", "Search for a custom SexpPath") do |sexp|
49
+ @scanner.add_query(sexp)
50
+ end
51
+
52
+ opts.separator ""
53
+ opts.separator "Options:"
54
+ opts.on("-R", "Recursively search folders") do
55
+ @recurse = true
56
+ end
57
+
58
+ opts.on("--no-cache", "Do not use a cache") do
59
+ @cache_path = nil
60
+ end
61
+
62
+ opts.on("--cache PATH", "Use the cache at PATH (defaults to current dir)", "Beware, the cache can get rather large") do |path|
63
+ @cache_path = path if path
64
+ end
65
+
66
+ opts.on("-v", "--verbose", "Verbose output") do
67
+ @scanner.verbose = true
68
+ end
69
+
70
+ opts.on_tail("-h", "--help", "Show this message") do
71
+ puts opts
72
+ exit
73
+ end
74
+ end
75
+ opts.parse!(args)
76
+
77
+ @paths = args
78
+
79
+ if @paths.empty?
80
+ puts opts
81
+ exit 1
82
+ end
83
+
84
+ @paths = expand_paths(@paths) if @recurse
85
+ @scanner.cache = RubyScope::SexpCache.new(@cache_path) if @cache_path
86
+ end
87
+
88
+ def run
89
+ @paths.each do |path|
90
+ @scanner.scan path
91
+ end
92
+ end
93
+
94
+ protected
95
+ def expand_paths(paths)
96
+ paths.inject([]){|p,v| File.directory?(v) ? p.concat(Dir[File.join(v,'**/*.rb')]) : p << v; p }
97
+ end
98
+
99
+ # Inserts the appropriate type of value given name.
100
+ # For instance:
101
+ # v('/cake/') #=> /cake/ # regular expression match
102
+ # v('apple') #=> :apple # atom match
103
+ def v(name)
104
+ if name =~ /^\/.+\/$/ # regular expression matching regular expression... WIN!
105
+ "m(#{name})"
106
+ else
107
+ ":#{name}"
108
+ end
109
+ end
110
+
111
+ def instance_variable?(name)
112
+ name[0..0] == '@'
113
+ end
114
+
115
+ end
@@ -0,0 +1,92 @@
1
+ class RubyScope::Scanner
2
+ attr_accessor :verbose
3
+ attr_accessor :cache
4
+
5
+ def initialize
6
+ @query = nil
7
+ @verbose = false
8
+ end
9
+
10
+ def add_query(pattern)
11
+ # Generate the pattern, we use a little instance_eval trickery here.
12
+ sexp = case pattern
13
+ when String then SexpPath::SexpQueryBuilder.instance_eval(pattern)
14
+ when Sexp then pattern
15
+ else raise ArgumentError, "Expected a String or Sexp"
16
+ end
17
+
18
+ if @query
19
+ @query = @query | sexp
20
+ else
21
+ @query = sexp
22
+ end
23
+ @query
24
+
25
+ rescue Exception=>ex
26
+ puts "Invalid Pattern: '#{pattern}'"
27
+ puts "Trace:"
28
+ puts ex
29
+ puts ex.backtrace
30
+ exit 1
31
+ end
32
+
33
+ def query
34
+ @query.clone if @query
35
+ end
36
+
37
+ def scan(path)
38
+ @path = path
39
+ begin
40
+ report_file path
41
+
42
+ # Reset our cached code and split lines
43
+ @code,@lines = nil,nil
44
+
45
+ # Load the code and parse it with RubyParser
46
+ # If we're caching pull from the cache, otherwise parse the code
47
+ sexp = @cache[path] if @cache
48
+ if !sexp
49
+ sexp = RubyParser.new.parse(code, @path)
50
+ @cache[path] = sexp if @cache
51
+ end
52
+
53
+ if sexp
54
+ # Search it with the given pattern, printing any results
55
+ sexp.search_each(@query) do |matching_sexp|
56
+ report_match matching_sexp
57
+ end
58
+ end
59
+ rescue StandardError => ex
60
+ report_exception ex
61
+ end
62
+ end
63
+
64
+ protected
65
+ def report_file(path)
66
+ puts @path if @verbose
67
+ end
68
+
69
+ def report_match(match)
70
+ if !@lines
71
+ puts @path unless @verbose
72
+ @lines = code.split("\n")
73
+ end
74
+ line_number = match.sexp.line - 1
75
+ puts "%4i: %s" % [match.sexp.line, @lines[line_number].strip]
76
+ end
77
+
78
+ def report_exception(ex)
79
+ debug "Problem processing '#{@path}'"
80
+ debug ex.message.strip
81
+ debug ex.backtrace.map{|line| " #{line}"}.join("\n")
82
+ end
83
+
84
+ def code
85
+ @code ||= File.read(@path)
86
+ end
87
+
88
+ def debug(msg)
89
+ STDERR.print(msg.to_s.chomp + "\n")
90
+ end
91
+
92
+ end
@@ -0,0 +1,45 @@
1
+ require 'dbm'
2
+ class RubyScope::SexpCache
3
+ DB = DBM
4
+ CacheEntry = Struct.new(:last_modified, :sexp)
5
+
6
+ def initialize(root)
7
+ @root = root
8
+ end
9
+
10
+ def [] path
11
+ with_cache do |cache|
12
+ entry = cache[path]
13
+ entry = Marshal.load(entry) if entry
14
+
15
+ if entry && entry.last_modified == last_modified(path)
16
+ entry.sexp
17
+ else
18
+ cache.delete path
19
+ nil
20
+ end
21
+ end
22
+ end
23
+
24
+ def []= path,value
25
+ with_cache do |cache|
26
+ entry = CacheEntry.new(last_modified(path), value)
27
+ cache[path] = Marshal.dump(entry)
28
+ end
29
+ end
30
+
31
+ def cache_path
32
+ @cache_path ||= File.join(@root,'.ruby_scope.cache')
33
+ end
34
+
35
+ protected
36
+ def with_cache
37
+ DB.open(cache_path, 0666) do |cache|
38
+ yield cache
39
+ end
40
+ end
41
+
42
+ def last_modified(path)
43
+ File.mtime(path)
44
+ end
45
+ end
@@ -0,0 +1,51 @@
1
+ require 'test/test_helper'
2
+
3
+ # Self referential testing!
4
+ # These are awesome, absurd, and abusive.
5
+ # Make sure that the app actually does what we say it does.
6
+ class ConsoleTest < Test::Unit::TestCase
7
+ ROOT = File.dirname(__FILE__) + '/../../'
8
+
9
+ def test_finding_this_test
10
+ res = rs "--def '/^test/'"
11
+ assert_success
12
+ assert res['test_finding_this_test'], "Should have found this test"
13
+ end
14
+
15
+ def test_finding_assignment
16
+ self_referential_line_number = __LINE__
17
+
18
+ res = rs "--assign self_referential_line_number"
19
+ assert_success
20
+ assert res.split("\n").last =~ /(\d+)\:\s+self_referential_line_number = __LINE__/
21
+ assert_equal self_referential_line_number, $1.to_i
22
+ end
23
+
24
+ def test_reading_the_help_and_making_queries
25
+ help = rs "--help"
26
+ assert help =~ /Queries:(.+)\s+Options:/m, "Should have a queries section"
27
+ queries = $1
28
+ queries.split("\n").each do |query|
29
+ next if query =~ /^\s*$/
30
+ assert query =~ /(--\S+)\s+(\S+)?/, "Each query should be a long flag with an optional parameter\n#{query.inspect}"
31
+ flag, param_type = $1,$2
32
+ param = case param_type
33
+ when 'NAME' then "a"
34
+ when 'SEXP_PATH' then "'s()'"
35
+ else nil
36
+ end
37
+
38
+ res = rs "#{flag} #{param}"
39
+ assert_success res
40
+ end
41
+ end
42
+
43
+ private
44
+ def rs(args)
45
+ `#{ROOT}bin/ruby_scope --no-cache #{args} #{__FILE__}`
46
+ end
47
+
48
+ def assert_success(msg=nil)
49
+ assert $?.success?, msg||"Exited with status #{$?.exitstatus}"
50
+ end
51
+ end
@@ -0,0 +1,5 @@
1
+ require 'test/unit'
2
+ require File.dirname(__FILE__) + '/../lib/ruby_scope'
3
+
4
+ require 'rubygems'
5
+ require 'mocha'
@@ -0,0 +1,41 @@
1
+ require 'test/test_helper'
2
+
3
+ class CLITest < Test::Unit::TestCase
4
+ def test_configuring_scanner
5
+ query = scanner_with('--def','cats','.').query
6
+ assert query, "Should have generated a query"
7
+ end
8
+
9
+ def test_configuring_scanner_with_regex
10
+ query = scanner_with('--def','/cats/','.').query
11
+ assert query, "Should have generated a query"
12
+ end
13
+
14
+ def test_disabling_cache
15
+ c = cli_with('--no-cache', '.')
16
+ assert !c.cache_path
17
+ assert !c.scanner.cache
18
+ end
19
+
20
+ def test_default_cache_behavior
21
+ c = cli_with('.')
22
+ assert c.cache_path
23
+ assert c.scanner.cache
24
+ end
25
+
26
+ def test_custom_cache_path
27
+ path = File.dirname(__FILE__) + '../lib'
28
+ c = cli_with('--cache', path, '.')
29
+ assert_equal path, c.cache_path
30
+ assert c.scanner.cache
31
+ end
32
+
33
+ private
34
+ def cli_with(*args)
35
+ RubyScope::CLI.new(args)
36
+ end
37
+
38
+ def scanner_with(*args)
39
+ cli_with(*args).scanner
40
+ end
41
+ end
@@ -0,0 +1,33 @@
1
+ require 'test/test_helper'
2
+
3
+ class ScannerTest < Test::Unit::TestCase
4
+ def setup
5
+ @scanner = RubyScope::Scanner.new
6
+ @scanner.cache = {}
7
+ end
8
+
9
+ def test_scanning_cache_hit_with_no_match
10
+ path = 'cached_sample.rb'
11
+ @scanner.cache[path] = s(:a)
12
+ @scanner.add_query( s(:b) )
13
+
14
+ # Should have no hit
15
+ @scanner.expects(:report_match).never
16
+ # Should not try to read the file since it is cached
17
+ @scanner.expects(:code).never
18
+
19
+ @scanner.scan(path)
20
+ end
21
+
22
+ def test_scanning_cache_hit_with_match
23
+ path = 'cached_sample.rb'
24
+ @scanner.cache[path] = s(:a)
25
+ @scanner.add_query( s(:a) )
26
+
27
+ # Should report a match
28
+ @scanner.expects(:report_match)
29
+
30
+ @scanner.scan(path)
31
+ end
32
+
33
+ end
@@ -0,0 +1,42 @@
1
+ require 'test/test_helper'
2
+
3
+ class SexpCacheTest < Test::Unit::TestCase
4
+ def setup
5
+ @path = File.dirname(__FILE__)
6
+ @cache = RubyScope::SexpCache.new(@path)
7
+ @cache.stubs(:last_modified).returns('ok')
8
+ end
9
+
10
+ def teardown
11
+ File.delete(@cache.cache_path) if File.exists? @cache.cache_path
12
+ end
13
+
14
+ def test_misses
15
+ assert_equal nil, @cache['missing']
16
+ end
17
+
18
+ def test_writing_and_reading
19
+ key,value = 'a',s(:code)
20
+
21
+ @cache[key] = value
22
+ assert_equal value, @cache[key]
23
+ end
24
+
25
+ def test_expired_reads
26
+ key,value = 'a',s(:code)
27
+
28
+ @cache[key] = value
29
+ # now invalidate the key
30
+ @cache.stubs(:last_modified).returns('old')
31
+
32
+ assert_equal nil, @cache[key]
33
+ end
34
+
35
+ def test_over_writing_and_reading
36
+ key,value1,value2 = 'a',s(:code),s(:new_code)
37
+
38
+ @cache[key] = value1
39
+ @cache[key] = value2
40
+ assert_equal value2, @cache[key]
41
+ end
42
+ end
metadata ADDED
@@ -0,0 +1,109 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ruby_scope
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.1
5
+ platform: ruby
6
+ authors:
7
+ - Adam Sanderson
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2010-04-17 00:00:00 -07:00
13
+ default_executable: ruby_scope
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: sexp_processor
17
+ type: :runtime
18
+ version_requirement:
19
+ version_requirements: !ruby/object:Gem::Requirement
20
+ requirements:
21
+ - - ~>
22
+ - !ruby/object:Gem::Version
23
+ version: "3.0"
24
+ version:
25
+ - !ruby/object:Gem::Dependency
26
+ name: ruby_parser
27
+ type: :runtime
28
+ version_requirement:
29
+ version_requirements: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ~>
32
+ - !ruby/object:Gem::Version
33
+ version: "2.0"
34
+ version:
35
+ - !ruby/object:Gem::Dependency
36
+ name: sexp_path
37
+ type: :runtime
38
+ version_requirement:
39
+ version_requirements: !ruby/object:Gem::Requirement
40
+ requirements:
41
+ - - ">="
42
+ - !ruby/object:Gem::Version
43
+ version: "0.4"
44
+ version:
45
+ - !ruby/object:Gem::Dependency
46
+ name: mocha
47
+ type: :development
48
+ version_requirement:
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: 0.9.8
54
+ version:
55
+ description: " A ruby hacker's search tool. Quickly interrogate your code, seek out \n classes, methods, variable assignments, and more.\n"
56
+ email: netghost@gmail.com
57
+ executables:
58
+ - ruby_scope
59
+ extensions: []
60
+
61
+ extra_rdoc_files:
62
+ - README.markdown
63
+ files:
64
+ - README.markdown
65
+ - Rakefile
66
+ - VERSION
67
+ - bin/ruby_scope
68
+ - lib/ruby_scope.rb
69
+ - lib/ruby_scope/cli.rb
70
+ - lib/ruby_scope/scanner.rb
71
+ - lib/ruby_scope/sexp_cache.rb
72
+ - test/integration/console_test.rb
73
+ - test/test_helper.rb
74
+ - test/unit/cli_test.rb
75
+ - test/unit/scanner_test.rb
76
+ - test/unit/sexp_cache_test.rb
77
+ has_rdoc: true
78
+ homepage: http://github.com/adamsanderson/ruby_scope
79
+ licenses: []
80
+
81
+ post_install_message:
82
+ rdoc_options:
83
+ - --charset=UTF-8
84
+ require_paths:
85
+ - lib
86
+ required_ruby_version: !ruby/object:Gem::Requirement
87
+ requirements:
88
+ - - ">="
89
+ - !ruby/object:Gem::Version
90
+ version: "0"
91
+ version:
92
+ required_rubygems_version: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: "0"
97
+ version:
98
+ requirements: []
99
+
100
+ rubyforge_project:
101
+ rubygems_version: 1.3.5
102
+ signing_key:
103
+ specification_version: 3
104
+ summary: Ruby colored binoluars for your code.
105
+ test_files:
106
+ - test/integration/console_test.rb
107
+ - test/unit/cli_test.rb
108
+ - test/unit/scanner_test.rb
109
+ - test/unit/sexp_cache_test.rb