rbr 0.1.0 → 0.2.0

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.
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
  - - ">="