rbr 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: '0586dc665c13aa156f7997779e6b2eccda40556896ef39b445c6b6d1160cd33a'
4
- data.tar.gz: 10413f9d6eab1a7cfa5376b79ec37cbc91984a3915e59a850aced3c6a3515642
3
+ metadata.gz: 8f991cd863713e0995b19ec6dd66772030342e9af5ef18441ed14afe7c68a24d
4
+ data.tar.gz: a12bb9d633f5b33dbd78034a26915e4168deefa306d5ec567adf672b02d4cf34
5
5
  SHA512:
6
- metadata.gz: eb5c7da4463d013eda03ca6243189cbea36ecb5e6b70d5a30ea7e3c02030456bb3dcbe715651d1c18216d6bc120292b2d5e4256abe5349403078d3d5b420d341
7
- data.tar.gz: 27730f7b04550f8e11897b685db8f2bea44f3f65ff3402a3adee045b113c185dad1e90399d6d008fda0b60cffe7f6859533f4d5b2edeaa7191c95a3004f12c58
6
+ metadata.gz: 36c06cff179b606e84e217adfc1233f0fe9b817acb749c780230959445b7585ac6a39ac41e3ddef4a58399935d3ad8602b51fe1d3a5d13e9ea90e7251ff01f68
7
+ data.tar.gz: 76040e738fb9c97400836106c368eafa9e1a93112622d9f13ef86f77f9cd9d54a477a4bb9f42c7dc5f237c14a99ea4cbe3adfe4d767b354cbd84e3d6a547a80f
@@ -1,4 +1,18 @@
1
1
  ---
2
+ require:
3
+ - rubocop-minitest
4
+ - rubocop-performance
5
+
6
+ AllCops:
7
+ NewCops: enable
8
+ TargetRubyVersion: 2.6
9
+ Exclude:
10
+ - "test/fixtures/*"
11
+
12
+ Metrics/MethodLength:
13
+ Exclude:
14
+ - "test/**"
15
+
2
16
  Style/Documentation:
3
17
  Exclude:
4
18
  - "test/**"
data/Gemfile CHANGED
@@ -1,6 +1,8 @@
1
+ # frozen_string_literal: true
2
+
1
3
  source "https://rubygems.org"
2
4
 
3
- git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
5
+ git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }
4
6
 
5
7
  # Specify your gem's dependencies in rbr.gemspec
6
8
  gemspec
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- rbr (0.1.0)
4
+ rbr (0.2.0)
5
5
  parser
6
6
 
7
7
  GEM
@@ -16,7 +16,7 @@ GEM
16
16
  pry (0.13.1)
17
17
  coderay (~> 1.1)
18
18
  method_source (~> 1.0)
19
- rake (10.5.0)
19
+ rake (13.0.1)
20
20
 
21
21
  PLATFORMS
22
22
  ruby
@@ -25,7 +25,7 @@ DEPENDENCIES
25
25
  bundler (~> 1.17)
26
26
  minitest (~> 5.0)
27
27
  pry (~> 0.0)
28
- rake (~> 10.0)
28
+ rake (~> 13.0)
29
29
  rbr!
30
30
 
31
31
  BUNDLED WITH
data/README.md CHANGED
@@ -1,35 +1,67 @@
1
1
  # Rbr
2
2
 
3
- Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/rbr`. To experiment with that code, run `bin/console` for an interactive prompt.
3
+ Rbr is a code search tool that parses Ruby code so you can query over certain semantic
4
+ constructs.
5
+
6
+ ## Usage examples
7
+
8
+ ```sh
9
+ # assignment to an lvalue named `@author`
10
+ $ rbr assignment :@author test/fixtures/book.rb
11
+ 5: @author = author
12
+
13
+ # int or float
14
+ $ rbr number 5 test/fixtures/book.rb
15
+ 12: 5
16
+
17
+ # string
18
+ $ rbr string ring test/fixtures/book.rb
19
+ 13: "a string!"
20
+
21
+ # a literal (int, float, or string)
22
+ $ rbr literal 5 test/fixtures/book.rb
23
+ 12: 5
24
+ 49: "5"
25
+
26
+ # comments matching the pattern /great/
27
+ $ rbr comment great test/fixtures/book.rb
28
+ 1: # This is a great class
29
+
30
+ # find all calls of a method named `great_method`
31
+ $ rbr method_call :great_method test/fixtures/book.rb
32
+ 27: book.update!(title: "Great Title")
33
+ 50: book.send(:update!, title: "Great Title")
34
+
35
+ # statements that update an ActiveRecord model attribute named `title`
36
+ $ rbr ar_update :title test/fixtures/book.rb
37
+ 21: book.title = "Great Title"
38
+ 27: book.update!(title: "Great Title")
39
+ 31: book.send(:update_column, :title, "Great Title")
40
+ ```
4
41
 
5
- TODO: Delete this and the text above, and describe your gem
42
+ rbr is the wrong tool for the following situations:
6
43
 
7
- ## Installation
44
+ ```sh
45
+ # find any appearance of the string "author" in the source
46
+ $ grep "author"
8
47
 
9
- Add this line to your application's Gemfile:
48
+ # find a symbol named :author
49
+ $ grep ":author"
10
50
 
11
- ```ruby
12
- gem 'rbr'
51
+ # find the definition of any function named "publish"
52
+ $ grep "def publish"
13
53
  ```
14
54
 
15
- And then execute:
16
-
17
- $ bundle
18
-
19
- Or install it yourself as:
20
-
21
- $ gem install rbr
55
+ ## Installation
22
56
 
23
- ## Usage
57
+ Rbr is intended to be used as a command-line program. Install the executable `rbr`
58
+ with:
24
59
 
25
- TODO: Write usage instructions here
60
+ ```
61
+ gem install rbr
62
+ ```
26
63
 
27
64
  ## Development
28
65
 
29
- After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
30
-
31
- 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).
32
-
33
- ## Contributing
34
-
35
- Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/rbr.
66
+ After checking out the repo, run `bundle install` to install dependencies. Then,
67
+ run `rake test` to run the tests or `bundle exec rbr` to run the local copy.
data/Rakefile CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "bundler/gem_tasks"
2
4
  require "rake/testtask"
3
5
 
@@ -7,4 +9,4 @@ Rake::TestTask.new(:test) do |t|
7
9
  t.test_files = FileList["test/**/*_test.rb"]
8
10
  end
9
11
 
10
- task :default => :test
12
+ task default: :test
data/TODO ADDED
@@ -0,0 +1 @@
1
+ not_str matcher
data/exe/rbr CHANGED
@@ -5,4 +5,4 @@
5
5
  require "parser/current"
6
6
  require "rbr/cli"
7
7
 
8
- Rbr::CLI.start
8
+ Rbr::CLI.new.start
@@ -1,29 +1,44 @@
1
- #!/usr/bin/env ruby
2
-
3
1
  # frozen_string_literal: true
4
2
 
5
3
  require "parser/current"
6
- require "rbr/query"
7
4
 
8
- # A semantically-aware code search tool for Ruby
5
+ require "rbr/query"
9
6
 
10
7
  module Rbr
11
8
  class CLI
12
- def self.start
13
- return unless check_arg_count
9
+ def initialize
10
+ check_arg_count
11
+
12
+ @matcher = ARGV[0].to_sym
13
+ @condition = ARGV[1]
14
+ end
14
15
 
15
- matcher = ARGV[0].to_sym
16
- name = ARGV[1].to_sym
17
- root, comments = Parser::CurrentRuby.parse_file_with_comments(ARGV[2])
16
+ def start
17
+ filenames.each do |filename|
18
+ root, comments = Parser::CurrentRuby.parse_file_with_comments(filename)
18
19
 
19
- nodes = Query.new(matcher => { name: name }).run(root)
20
+ matching_nodes = Query.new(@matcher, @condition).run(root, comments)
20
21
 
21
- nodes.each do |node|
22
- puts node.pretty_print
22
+ matching_nodes.each { |node| puts node.pretty_print }
23
+ rescue EncodingError
24
+ warn "# Encoding error parsing #{filename}"
23
25
  end
24
26
  end
25
27
 
26
- def self.check_arg_count
28
+ def filenames
29
+ ARGV[2..].map { |arg| expand_path(arg) }
30
+ .flatten
31
+ .select { |filename| File.file?(filename) }
32
+ end
33
+
34
+ def expand_path(path)
35
+ return [path] if File.file?(path)
36
+
37
+ # if path is not a file, then glob all .rb files beneath it
38
+ Dir.glob("#{path}/**/*.rb")
39
+ end
40
+
41
+ def check_arg_count
27
42
  return true if ARGV.count >= 3
28
43
 
29
44
  warn <<~USAGE
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rbr
4
+ # Wraps a Parser::Source::Comment object and provides convenience methods
5
+ class CommentNode
6
+ def initialize(parser_comment)
7
+ @parser_comment = parser_comment
8
+ end
9
+
10
+ def expression
11
+ @parser_comment.loc.expression
12
+ end
13
+
14
+ def pretty_print
15
+ "#{expression.line}: #{@parser_comment.text}"
16
+ end
17
+ end
18
+ end
@@ -3,71 +3,106 @@
3
3
  module Rbr
4
4
  # methods for determining if a node matches given conditions
5
5
  class Matchers
6
- def self.match(node, query)
7
- query.map do |matcher, conds|
8
- send(matcher, node, conds)
9
- end.all? #TODO any? or all?
6
+ def self.match(node, matcher, condition)
7
+ send(matcher, node, condition)
8
+ end
9
+
10
+ # Method call
11
+ def self.method_call(node, name)
12
+ name && node.method_call?(name)
10
13
  end
11
14
 
12
15
  # Updating an ActiveRecord model attribute
13
16
  def self.ar_update(node, name)
14
- attribute_name = conds[:name]
15
- matches = if attribute_name
16
- node.children.first == attribute_name
17
- else
18
- true # everything matches if no condition
19
- end
20
-
21
- node.method_call? &&
22
- node.children[1] == :update &&
23
- node.children[1].is_a_hash_containing_node(children: [:name])
17
+ name &&
18
+ (
19
+ ar_update_hash(node, name) ||
20
+ ar_update_positional(node, name) ||
21
+ ar_update_dynamic_method(node, name) ||
22
+ ar_update_attributes(node, name) ||
23
+ ar_update_hash_element(node, name)
24
+ )
24
25
  end
25
26
 
26
27
  # Assignment to a specified lvalue
27
- def self.assignment(node, conds)
28
- return false unless node.assignment?
29
-
30
- !conds || node.children.first == conds[:name]
28
+ def self.assignment(node, name)
29
+ name &&
30
+ node.assignment? &&
31
+ node.value == name
31
32
  end
32
33
 
33
34
  # Node is a literal int, float, or string
34
- # TODO rename. Literal?
35
- def self.is(node, conds)
36
- return false unless node.literal?
35
+ def self.literal(node, value)
36
+ number(node, value) || str(node, value)
37
+ end
37
38
 
38
- !conds ||
39
- # Ruby symbols can't start with a number, so #to_s first
40
- node.children.first.to_s.to_sym == conds[:name]
39
+ # Node is a literal int or float
40
+ def self.number(node, value)
41
+ value &&
42
+ node.number? &&
43
+ node.value.to_s == value
41
44
  end
42
45
 
43
46
  # Node is a Ruby constant
44
- def self.const(node, conds)
45
- return false unless node.const?
46
-
47
- !conds || node.children.last == conds[:name]
47
+ def self.const(node, name)
48
+ name &&
49
+ node.const? &&
50
+ node.children.last == name
48
51
  end
49
52
 
50
53
  # Node is a string
51
- def self.str(node, conds)
52
- return false unless node.str?
54
+ def self.str(node, pattern)
55
+ pattern &&
56
+ node.str? &&
57
+ node.any_descendant_matches?(
58
+ ->(n) { n.is_a?(String) && n.match?(pattern) }
59
+ )
60
+ end
61
+
62
+ private
53
63
 
54
- !conds || node.any_child_matches?(
55
- ->(n) { n.is_a?(String) && n.match?(conds[:pattern]) }
64
+ private_class_method def self.ar_update_hash(node, name)
65
+ return false unless node.method_call?(
66
+ %i[update update! assign_attributes update_attributes update_attributes!
67
+ update_columns update_all upsert upsert_all insert insert! insert_all
68
+ insert_all!]
56
69
  )
70
+
71
+ hash_arg = if node.children[3]&.type == :hash
72
+ node.children[3]
73
+ else
74
+ node.children[2]
75
+ end
76
+
77
+ return false unless hash_arg.is_a?(Node)
78
+
79
+ hash_arg.children.any? do |child|
80
+ child.is_a?(Node) && child.children[0].value == name
81
+ end
57
82
  end
58
83
 
59
- # Node is a comment
60
- def self.comment(node, conds)
61
- return false unless node.comment?
84
+ private_class_method def self.ar_update_positional(node, name)
85
+ return false unless node.method_call?(
86
+ %i[write_attribute update_attribute update_column]
87
+ )
62
88
 
63
- #TODO
89
+ node.children[2].value == name
64
90
  end
65
91
 
66
- # Anything other than a string
67
- def self.not_str(node, conds)
68
- return false if node.str?
92
+ private_class_method def self.ar_update_dynamic_method(node, name)
93
+ node.method_call?("#{name}=".to_sym)
94
+ end
95
+
96
+ private_class_method def self.ar_update_attributes(node, name)
97
+ node.method_call?(:attributes=) &&
98
+ node.children.last.type == :hash &&
99
+ node.children.last.children.any? do |child|
100
+ child.is_a?(Node) && child.children[0].value == name
101
+ end
102
+ end
69
103
 
70
- !conds || node.children.last == conds[:name]
104
+ private_class_method def self.ar_update_hash_element(node, name)
105
+ node.method_call?(:[]=) && node.children[-2].value == name
71
106
  end
72
107
  end
73
108
  end
@@ -12,15 +12,25 @@ module Rbr
12
12
  end
13
13
 
14
14
  def const?
15
- :const == @ast_node.type
15
+ @ast_node.type == :const
16
16
  end
17
17
 
18
18
  def literal?
19
19
  %i[int float str].include? @ast_node.type
20
20
  end
21
21
 
22
- def method_call?
23
- %i[send csend].include? @ast_node.type
22
+ def number?
23
+ %i[int float].include? @ast_node.type
24
+ end
25
+
26
+ def method_call?(names)
27
+ %i[send csend].include?(@ast_node.type) &&
28
+ begin
29
+ names = [names] unless names.is_a?(Array)
30
+
31
+ names.include?(children[1]) ||
32
+ (children[1] == :send && names.include?(children[2].value))
33
+ end
24
34
  end
25
35
 
26
36
  def nil?
@@ -31,19 +41,37 @@ module Rbr
31
41
  %i[str dstr xstr].include? @ast_node.type
32
42
  end
33
43
 
44
+ def type
45
+ @ast_node.type
46
+ end
47
+
48
+ def expression
49
+ @ast_node.loc.expression
50
+ end
51
+
34
52
  def children
35
- @ast_node.children
53
+ @ast_node.children.map do |child|
54
+ if child.is_a?(Parser::AST::Node)
55
+ Rbr::Node.new(child)
56
+ else
57
+ child
58
+ end
59
+ end
60
+ end
61
+
62
+ def value
63
+ @ast_node.children[0]
36
64
  end
37
65
 
38
66
  def pretty_print
39
- "#{@ast_node.loc.expression.line}: #{@ast_node.loc.expression.source}"
67
+ "#{expression.line}: #{expression.source}"
40
68
  end
41
69
 
42
70
  # Call the the proc, passing in this node and all children recursively. Return
43
71
  # true if any call evaluates to true.
44
- def any_child_matches?(match_proc, root = self)
72
+ def any_descendant_matches?(match_proc, root = self)
45
73
  if root.respond_to?(:children)
46
- root.children.any? { |c| any_child_matches?(match_proc, c) }
74
+ root.children.any? { |c| any_descendant_matches?(match_proc, c) }
47
75
  else
48
76
  match_proc.call(root)
49
77
  end
@@ -1,21 +1,38 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "rbr/matchers"
4
+ require "rbr/comment_node"
4
5
  require "rbr/node"
5
6
 
6
7
  module Rbr
7
8
  class Query
8
- attr_reader :conditions
9
+ attr_reader :matcher, :condition
9
10
 
10
- def initialize(conditions)
11
- @conditions = conditions
11
+ def initialize(matcher, condition)
12
+ @matcher = matcher
13
+ @condition = condition
14
+
15
+ if matcher == :comment
16
+ alias run run_comments
17
+ else
18
+ alias run run_tree
19
+
20
+ if condition.is_a?(String) && condition.start_with?(":")
21
+ @condition = condition[1..].to_sym
22
+ end
23
+ end
24
+ end
25
+
26
+ def run_comments(_node, comments)
27
+ comments.select { |comment| comment.text.match?(condition) }
28
+ .map { |comment| CommentNode.new(comment) }
12
29
  end
13
30
 
14
- def run(node)
31
+ def run_tree(node, _comments = [])
15
32
  node = Node.new(node) unless node.is_a?(Node)
16
33
  return [] if node.nil?
17
34
 
18
- node_matches = Matchers.match(node, conditions)
35
+ node_matches = Matchers.match(node, matcher, condition)
19
36
  found_children = node.children.map { |child| run(child) }
20
37
 
21
38
  (node_matches ? [node] : []) + found_children.flatten
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Rbr
2
- VERSION = "0.1.0"
4
+ VERSION = "0.2.0"
3
5
  end
@@ -31,11 +31,12 @@ Gem::Specification.new do |spec|
31
31
  spec.bindir = "exe"
32
32
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
33
33
  spec.require_paths = ["lib"]
34
+ spec.required_ruby_version = ">= 2.6.0"
34
35
 
35
36
  spec.add_dependency "parser"
36
37
 
37
38
  spec.add_development_dependency "bundler", "~> 1.17"
38
39
  spec.add_development_dependency "minitest", "~> 5.0"
39
40
  spec.add_development_dependency "pry", "~> 0.0"
40
- spec.add_development_dependency "rake", "~> 10.0"
41
+ spec.add_development_dependency "rake", "~> 13.0"
41
42
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rbr
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Eddie Lebow
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2020-06-05 00:00:00.000000000 Z
11
+ date: 2020-06-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: parser
@@ -72,14 +72,14 @@ dependencies:
72
72
  requirements:
73
73
  - - "~>"
74
74
  - !ruby/object:Gem::Version
75
- version: '10.0'
75
+ version: '13.0'
76
76
  type: :development
77
77
  prerelease: false
78
78
  version_requirements: !ruby/object:Gem::Requirement
79
79
  requirements:
80
80
  - - "~>"
81
81
  - !ruby/object:Gem::Version
82
- version: '10.0'
82
+ version: '13.0'
83
83
  description:
84
84
  email:
85
85
  - elebow@users.noreply.github.com
@@ -94,8 +94,10 @@ files:
94
94
  - Gemfile.lock
95
95
  - README.md
96
96
  - Rakefile
97
+ - TODO
97
98
  - exe/rbr
98
99
  - lib/rbr/cli.rb
100
+ - lib/rbr/comment_node.rb
99
101
  - lib/rbr/matchers.rb
100
102
  - lib/rbr/node.rb
101
103
  - lib/rbr/query.rb
@@ -115,7 +117,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
115
117
  requirements:
116
118
  - - ">="
117
119
  - !ruby/object:Gem::Version
118
- version: '0'
120
+ version: 2.6.0
119
121
  required_rubygems_version: !ruby/object:Gem::Requirement
120
122
  requirements:
121
123
  - - ">="