cql_ruby 0.0.4 → 0.0.9

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: 958c076c71b8288666a536478ac62399953865f9d9f829be68252bce88eb7625
4
- data.tar.gz: c4c6514cbef0488606b863654c2ad08471b75cd5b3dd2a0a9bb410e2a85a6551
3
+ metadata.gz: 4e459067630e91a0d52a1839445f881647e9c0271c052d973ccfafd6040f21b6
4
+ data.tar.gz: 39379431fb2d817930aff070a8335c02b62822d6bba0129956f6691cb5ea99b8
5
5
  SHA512:
6
- metadata.gz: 1cacdb6eb09004a14f2f5bcd4542a5a6935cf03ec95aaf3fbbcd9e61a6047f9f102a17e70575758fce2a15b57167260fc706176f92a0476f3a3185609f21320a
7
- data.tar.gz: 662ee75e93906a67bacda9cfdd8b992a59f2cc695e39bc1abb7bcc931b47cb1c51585f9d7fa6385043455b7cdc74fc512fb9895f70d1ebd4013997b192b53092
6
+ metadata.gz: 1de5455822e96348fad5fbda540e66ebf830723a4579927f8d4b1a2a9a4c9dd1c6d4763a514674eebff6974c8d61958e53af9fae74397fd643bb8a036f0a725d
7
+ data.tar.gz: 94c05fd4d565b1db7e793137495bd97456b1e459f9a58c6faba8c008c772a4898929ce5dc02d4064dec2ef0ee2ef0a7a2b68a2f9915430c681aca2a1a2bbb6f9
@@ -1,26 +1,39 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
+ require 'parser'
3
4
  require 'cql_ruby'
4
5
 
5
6
  def show_help
6
7
  puts <<~HELP
7
8
 
8
9
  \tSYNOPSIS
9
- \tcql_ruby options pattern path filters ...
10
+ \t\tcql_ruby options pattern path filters ...
10
11
 
11
12
  \tDESCRIPTION
12
- \tCQL (Code Query Language) is a semantic search tool for your Ruby source code.
13
+ \t\tCQL (Code Query Language) is a semantic search tool for your Ruby source code.
13
14
 
14
15
  \tFILTERS
15
- \t\tParent node type: type:T(,T)*
16
+ \t\tParent node type: type:T(,T)* Example: type:def,send,arg
17
+ \t\tNesting under: nest:T(=NAME) Example: nest:def=save_user nest:class=UserManager
18
+ \t\tHas child: has:T(=NAME) Example: has:const has:def=valid?
16
19
 
17
20
  \tOPTIONS
21
+ \t\t--include=PATTERN Parses only files whose name matches the pattern.
22
+ \t\t--exclude=PATTERN Parses only files whose name does not match the pattern.
23
+ \t\t-lN (N is integer) Add N surrounding line before and after.
18
24
  \t\t-nc (--no-color) No color on output.
19
25
  \t\t-nf (--no-file) No file names.
20
26
  \t\t-ns (--no-source) No source code.
27
+ \t\t-nr (--no-recursion) Non-recursive search.
28
+ \t\t-v -vv -vvv Debug output levels.
29
+
30
+ \tALLOWED NODE TYPES
31
+ \t\tWhen defining filters only valid AST types can be defined. They are:
32
+ #{Parser::Meta::NODE_TYPES.to_a.join(' ')}
21
33
 
22
34
  \tEXAMPLES
23
- \tcql_ruby -ns update_user_info ./ type:send,arg
35
+ \t\tcql_ruby user ./
36
+ \t\tcql_ruby -ns -nr %user_info ./ type:send,arg nest:block nest:class=r/User/i has:str=WARNING
24
37
  HELP
25
38
 
26
39
  exit
@@ -32,6 +45,10 @@ def extract_options
32
45
  show_color: true,
33
46
  show_file: true,
34
47
  show_source: true,
48
+ recursive_search: true,
49
+ surrounding_lines: 0,
50
+ include_pattern: nil,
51
+ exclude_pattern: nil,
35
52
  }
36
53
 
37
54
  ARGV.delete_if do |arg|
@@ -42,8 +59,19 @@ def extract_options
42
59
  options[:show_file] = false
43
60
  elsif %w[-ns --no-source].include?(arg)
44
61
  options[:show_source] = false
45
- elsif %w[-h --help]
62
+ elsif %w[-h --help].include?(arg)
46
63
  show_help
64
+ elsif %w[-v -vv -vvv].include?(arg)
65
+ lvl = arg.chars.find_all { |c| c == 'v' }.size
66
+ CqlRuby::Config.debug_level = lvl
67
+ elsif %w[-nr --no-recursive].include?(arg)
68
+ options[:recursive_search] = false
69
+ elsif arg[0..1] == '-l' && arg[2..].to_i > 0
70
+ options[:surrounding_lines] = arg[2..].to_i
71
+ elsif arg.start_with?('--include=')
72
+ options[:include_pattern] = arg.split('=')[1]
73
+ elsif arg.start_with?('--exclude=')
74
+ options[:exclude_pattern] = arg.split('=')[1]
47
75
  else
48
76
  raise "Unknown arg #{arg}"
49
77
  end
@@ -64,24 +92,39 @@ end
64
92
 
65
93
  begin
66
94
  options = extract_options
95
+ CqlRuby.log "Call options: #{options}" if CqlRuby::Config.debug_level_2?
67
96
 
68
97
  raise unless ARGV.size >= 2
69
98
 
70
99
  pattern = ARGV.shift
71
- # TODO Make path patterns universal.
100
+ CqlRuby.log "Call pattern: <#{pattern}>" if CqlRuby::Config.debug_level_2?
101
+
72
102
  path = ARGV.shift
103
+ CqlRuby.log "Call path: <#{path}>" if CqlRuby::Config.debug_level_2?
73
104
 
74
105
  # Rest must be filters - can sink ARGV now.
75
106
  filters = extract_filters
107
+ CqlRuby.log "Call filters: #{filters}" if CqlRuby::Config.debug_level_2?
108
+
76
109
  filter_reader = CqlRuby::FilterReader.new(filters)
77
110
 
78
111
  printer = CqlRuby::ConsolePrinter.new
79
112
  printer.color_on = options[:show_color]
80
113
  printer.file_on = options[:show_file]
81
114
  printer.source_on = options[:show_source]
115
+ printer.surrounding_lines = options[:surrounding_lines]
82
116
 
83
117
  collector = CqlRuby::CrumbCollector.new(printer)
84
- CqlRuby::Executor.new(collector, filter_reader, pattern, path, filters).search_all
118
+ CqlRuby::Executor.new(
119
+ collector: collector,
120
+ filter_reader: filter_reader,
121
+ pattern: pattern,
122
+ path: path,
123
+ filters: filters,
124
+ recursive: options[:recursive_search],
125
+ include: options[:include_pattern],
126
+ exclude: options[:exclude_pattern],
127
+ ).search_all
85
128
  rescue => e
86
129
  puts "Error: #{e}"
87
130
  show_help
@@ -1,6 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module CqlRuby; end
3
+ module CqlRuby;
4
+ def self.log(txt)
5
+ p txt
6
+ end
7
+ end
4
8
 
5
9
  require 'cql_ruby/executor'
6
10
  require 'cql_ruby/crumb_collector'
@@ -8,6 +8,7 @@ module CqlRuby
8
8
  attr_writer :color_on
9
9
  attr_writer :file_on
10
10
  attr_writer :source_on
11
+ attr_writer :surrounding_lines
11
12
 
12
13
  def initialize
13
14
  super
@@ -15,14 +16,33 @@ module CqlRuby
15
16
  @color_on = true
16
17
  @file_on = true
17
18
  @source_on = true
19
+ @surrounding_lines = 0
20
+ @counter = 0
18
21
  end
19
22
 
20
23
  #
21
24
  # @param crumb [Cqlruby::Crumb]
22
25
  #
23
26
  def print(crumb)
24
- puts "#{color(94)}#{crumb.file_name}#{decor_reset}:#{color(33)}#{crumb.line_no}#{decor_reset} #{color(93)}#{crumb.type}#{decor_reset}" if @file_on
25
- puts decorate_source_line(crumb) if @source_on
27
+ parts = "##{color(97)}#{@counter}#{decor_reset}"
28
+ parts += " #{color(94)}#{crumb.file_name}#{decor_reset}:#{color(33)}#{crumb.line_no}#{decor_reset} #{color(93)}#{crumb.type}#{decor_reset}" if @file_on
29
+
30
+ if @source_on && @surrounding_lines.positive?
31
+ parts_visible_len = parts.gsub(/\e\[\d+m/, '').size + 1
32
+ indent = ' ' * parts_visible_len
33
+ (-@surrounding_lines).upto(-1).each { |offs| puts "#{indent}#{crumb.surrounding_line(offs)}" }
34
+ end
35
+
36
+ parts += ' ' + decorate_source_line(crumb) if @source_on
37
+
38
+ puts parts
39
+
40
+ if @source_on && @surrounding_lines.positive?
41
+ 1.upto(@surrounding_lines).each { |offs| puts "#{indent}#{crumb.surrounding_line(offs)}" }
42
+ puts '--'
43
+ end
44
+
45
+ @counter += 1
26
46
  end
27
47
 
28
48
  private
@@ -54,7 +74,6 @@ module CqlRuby
54
74
  # @param [Cqlruby::Crumb] crumb
55
75
  # @return [String]
56
76
  def decorate_source_line(crumb)
57
- # TODO add +- line surrounding options
58
77
  source = crumb.source
59
78
  from = crumb.line_col_no
60
79
  to = from + crumb.expression_size
@@ -63,14 +82,14 @@ module CqlRuby
63
82
  subject = source[from..to - 1] || ''
64
83
  suffix = source[to..] || ''
65
84
 
66
- color(90) +
85
+ color(97) +
67
86
  prefix +
68
87
  decor_reset +
69
88
  color(31) +
70
89
  bold +
71
90
  subject +
72
91
  decor_reset +
73
- color(90) +
92
+ color(97) +
74
93
  suffix +
75
94
  decor_reset
76
95
  end
@@ -1,6 +1,21 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'parser/current'
4
+ require 'pathname'
5
+
6
+ module CqlRuby
7
+ class Config
8
+ @@debug_level = 0
9
+
10
+ class << self
11
+ def debug_level=(lvl); @@debug_level = lvl; end
12
+ def debug_level; @@debug_level; end
13
+ def debug_level_1?; @@debug_level >= 1; end
14
+ def debug_level_2?; @@debug_level >= 2; end
15
+ def debug_level_3?; @@debug_level >= 3; end
16
+ end
17
+ end
18
+ end
4
19
 
5
20
  #
6
21
  # Executes search and dumps results into the collector.
@@ -10,43 +25,85 @@ require 'parser/current'
10
25
  # @param path [String]
11
26
  # @param filters [Array<String>]
12
27
  #
13
- CqlRuby::Executor = Struct.new(:collector, :filter_reader, :pattern, :path, :filters) do
14
- def search_all
15
- files.flat_map do |file|
16
- search(file)
28
+ module CqlRuby
29
+ class Executor
30
+ def initialize(
31
+ collector:,
32
+ filter_reader:,
33
+ pattern:,
34
+ path:,
35
+ filters: [],
36
+ recursive: true,
37
+ include: nil,
38
+ exclude: nil
39
+ )
40
+ @collector = collector
41
+ @filter_reader = filter_reader
42
+ @pattern = pattern
43
+ @path = path
44
+ @filters = filters
45
+ @recursive = recursive
46
+ @include = include
47
+ @exclude = exclude
17
48
  end
18
- end
19
49
 
20
- private
50
+ def search_all
51
+ files.flat_map do |file|
52
+ next if !@exclude.nil? && CqlRuby::PatternMatcher.match?(@exclude, file)
53
+ next unless @include.nil? || CqlRuby::PatternMatcher.match?(@include, file)
21
54
 
22
- def search(file)
23
- ast = Parser::CurrentRuby.parse(File.read(file))
24
- source_reader = CqlRuby::SourceReader.new(file)
25
- walk(ast, [], source_reader)
55
+ CqlRuby.log "File check: #{file}" if CqlRuby::Config.debug_level_3?
56
+ search(file)
57
+ end
58
+ end
26
59
 
27
- nil
28
- end
60
+ private
29
61
 
30
- def walk(node, ancestors, source_reader)
31
- if node.is_a?(Parser::AST::Node)
32
- node.children.flat_map do |child|
33
- walk(child, ancestors.dup + [node], source_reader)
34
- end
35
- else
36
- if match?(node) && CqlRuby::FilterEvaluator.pass?(filter_reader, node, ancestors)
37
- collector.add(CqlRuby::Crumb.new(node, ancestors, source_reader))
62
+ def search(file)
63
+ ast = Parser::CurrentRuby.parse(File.read(file))
64
+ source_reader = CqlRuby::SourceReader.new(file)
65
+ walk(ast, [], source_reader)
66
+
67
+ nil
68
+ rescue => e
69
+ CqlRuby.log "File #{file} cannot be parsed"
70
+ CqlRuby.log "Reason: #{e}" if CqlRuby::Config.debug_level_1?
71
+ end
72
+
73
+ def walk(node, ancestors, source_reader)
74
+ if node.is_a?(Parser::AST::Node)
75
+ node.children.flat_map do |child|
76
+ walk(child, ancestors.dup + [node], source_reader)
77
+ end
78
+ else
79
+ if match?(node) && CqlRuby::FilterEvaluator.pass?(filter_reader, node, ancestors)
80
+ collector.add(CqlRuby::Crumb.new(node, ancestors, source_reader))
81
+ end
38
82
  end
83
+
84
+ nil
39
85
  end
40
86
 
41
- nil
42
- end
87
+ def match?(target)
88
+ CqlRuby::PatternMatcher.match?(pattern, target)
89
+ end
43
90
 
44
- def match?(target)
45
- CqlRuby::PatternMatcher.match?(pattern, target)
46
- end
91
+ def files
92
+ return [path] if File.file?(path)
93
+
94
+ clean_path = Pathname(path).cleanpath.to_s
95
+ clean_path += '/**' if recursive
96
+ clean_path += '/*.rb'
47
97
 
48
- def files
49
- Dir.glob(path)
98
+ Dir.glob(clean_path)
99
+ end
100
+
101
+ attr_reader :collector
102
+ attr_reader :filter_reader
103
+ attr_reader :pattern
104
+ attr_reader :path
105
+ attr_reader :filters
106
+ attr_reader :recursive
50
107
  end
51
108
  end
52
109
 
@@ -63,6 +120,10 @@ CqlRuby::Crumb = Struct.new(:full_name, :ancestors, :source_reader) do
63
120
  source_reader.source_line(line_no)
64
121
  end
65
122
 
123
+ def surrounding_line(offset)
124
+ source_reader.source_line(line_no + offset)
125
+ end
126
+
66
127
  def file_name
67
128
  source_reader.file
68
129
  end
@@ -82,6 +143,8 @@ CqlRuby::SourceReader = Struct.new(:file) do
82
143
  end
83
144
 
84
145
  def source_line(n)
146
+ return nil unless lines.size >= n
147
+
85
148
  lines[n - 1].chop
86
149
  end
87
150
 
@@ -7,6 +7,7 @@ module CqlRuby
7
7
  [
8
8
  pass_type?(filter_reader, ancestors),
9
9
  pass_nesting?(filter_reader, ancestors),
10
+ pass_has?(filter_reader, node, ancestors),
10
11
  ].all?
11
12
  end
12
13
 
@@ -25,7 +26,7 @@ module CqlRuby
25
26
  end
26
27
 
27
28
  #
28
- # @param [Cqlruby::FilterReader] filter_reader
29
+ # @param [CqlRuby::FilterReader] filter_reader
29
30
  # @param [Array<Parser::AST::Node>] ancestors
30
31
  #
31
32
  # @return [Boolean]
@@ -49,6 +50,79 @@ module CqlRuby
49
50
  end
50
51
  end
51
52
  end
53
+
54
+ #
55
+ # @param [CqlRuby::FilterReader] filter_reader
56
+ # @param [Parser::AST::Node] node
57
+ # @param [Array<Parser::AST::Node>] ancestors
58
+ #
59
+ def pass_has?(filter_reader, node, ancestors)
60
+ return true unless filter_reader.restrict_children?
61
+
62
+ filter_reader.has_leaves.all? do |has_rule|
63
+ anchor_node = try_get_class(ancestors) || try_get_module(ancestors) || try_get_def(ancestors)
64
+ next false unless anchor_node
65
+
66
+ has_node_with_name?(anchor_node, has_rule)
67
+ end
68
+ end
69
+
70
+ #
71
+ # @param [Array<Parser::AST::Node>] ancestors
72
+ #
73
+ def try_get_class(ancestors)
74
+ return nil unless ancestors.size >= 2
75
+ return nil unless ancestors[-1].type == :const
76
+ return nil unless ancestors[-2].type == :class
77
+
78
+ ancestors[-2].children[2]
79
+ end
80
+
81
+ #
82
+ # @param [Array<Parser::AST::Node>] ancestors
83
+ #
84
+ def try_get_module(ancestors)
85
+ return nil unless ancestors.size >= 2
86
+ return nil unless ancestors[-1].type == :const
87
+ return nil unless ancestors[-2].type == :module
88
+
89
+ ancestors[-2].children[1]
90
+ end
91
+
92
+ #
93
+ # @param [Array<Parser::AST::Node>] ancestors
94
+ #
95
+ def try_get_def(ancestors)
96
+ return nil unless ancestors.size >= 1
97
+ return nil unless ancestors[-1].type == :def
98
+
99
+ ancestors[-1].children[2]
100
+ end
101
+
102
+ #
103
+ # @param [Parser::AST::Node] anchor_node
104
+ # @param [CqlRuby::NodeSpec]
105
+ #
106
+ def has_node_with_name?(anchor_node, has_rule)
107
+ return false unless anchor_node.is_a?(Parser::AST::Node)
108
+
109
+ fn_children_with_type = ->(node) { node.children.map { |child| [child, node.type] } }
110
+ to_visit = fn_children_with_type.call(anchor_node)
111
+
112
+ until to_visit.empty?
113
+ current_node, current_type = to_visit.shift
114
+
115
+ if current_node.is_a?(Parser::AST::Node)
116
+ to_visit += fn_children_with_type.call(current_node)
117
+ else
118
+ if current_type == has_rule.type.to_sym && CqlRuby::PatternMatcher.match?(has_rule.name, current_node)
119
+ return true
120
+ end
121
+ end
122
+ end
123
+
124
+ false
125
+ end
52
126
  end
53
127
  end
54
128
  end
@@ -1,9 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # TODO: Have convenience filters for type:_ such as: isclass, ismodule, isdef ...
4
+
3
5
  module CqlRuby
4
- class NestRule < Struct.new(:type, :name)
6
+ class NodeSpec < Struct.new(:type, :name)
7
+ # Make this non duplicated.
5
8
  NAME_ANY = '*'
6
- ALLOWED_TYPE = %w[class module def block].freeze
7
9
 
8
10
  class << self
9
11
  #
@@ -15,8 +17,7 @@ module CqlRuby
15
17
  type, name = raw_value.split('=')
16
18
  name ||= NAME_ANY
17
19
 
18
- raise "Unknown type: #{type}. Allowed: #{ALLOWED_TYPE}" unless ALLOWED_TYPE.include?(type)
19
- raise "Type #{type} cannot have a name." if %w[block].include?(type) && name != NAME_ANY
20
+ raise "Type '#{type}' is not recognized. See 'cql_ruby --help' for allowed types." unless Parser::Meta::NODE_TYPES.member?(type.to_sym)
20
21
 
21
22
  new(type, name)
22
23
  end
@@ -38,16 +39,21 @@ module CqlRuby
38
39
  # example: type:def,send
39
40
  #
40
41
  class FilterReader
41
- # @attribute [Parser::AST::Node] allowed_types
42
+ NESTING_ALLOWED_TYPES = %w[class module def block].freeze
43
+
44
+ # @attribute [Array<Symbol>] allowed_types
42
45
  attr_reader :allowed_types
43
- # @attribute [Array<Cqlruby::NestRule>] nest_under
46
+ # @attribute [Array<CqlRuby::NodeSpec>] nest_under
44
47
  attr_reader :nest_under
48
+ # @attribute [Array<CqlRuby::NodeSpec>] has_leaves
49
+ attr_reader :has_leaves
45
50
 
46
51
  def initialize(raw_filters)
47
52
  super()
48
53
 
49
54
  @allowed_types = []
50
55
  @nest_under = []
56
+ @has_leaves = []
51
57
 
52
58
  parse_raw_filters(raw_filters)
53
59
  end
@@ -60,6 +66,10 @@ module CqlRuby
60
66
  !@nest_under.empty?
61
67
  end
62
68
 
69
+ def restrict_children?
70
+ !@has_leaves.empty?
71
+ end
72
+
63
73
  private
64
74
 
65
75
  # @param [Array<String>] raw_filters
@@ -71,7 +81,13 @@ module CqlRuby
71
81
  if %w[type t].include?(name)
72
82
  @allowed_types += value.split(',').map(&:to_sym)
73
83
  elsif %w[nest n].include?(name)
74
- @nest_under << NestRule.from(value)
84
+ spec = NodeSpec.from(value)
85
+ raise "Unknown type for nesting: '#{spec.type}' from '#{raw_filter}'. Allowed: #{NESTING_ALLOWED_TYPES}" unless NESTING_ALLOWED_TYPES.include?(spec.type)
86
+ raise "Type #{spec.type} cannot have a name." if %w[block].include?(spec.type) && spec.restrict_name?
87
+
88
+ @nest_under << spec
89
+ elsif %w[has h].include?(name)
90
+ @has_leaves << NodeSpec.from(value)
75
91
  end
76
92
  end
77
93
 
@@ -1,9 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
  module CqlRuby
3
3
  module PatternMatcher
4
+ MATCH_ANY = '*'
5
+
4
6
  def self.match?(pattern, subject)
5
- subject = subject.to_s
6
7
  pattern = pattern.to_s
8
+ return true if pattern == MATCH_ANY
9
+
10
+ subject = subject.to_s
7
11
 
8
12
  if regex?(pattern)
9
13
  regex_match?(pattern, subject)
@@ -15,7 +19,7 @@ module CqlRuby
15
19
  end
16
20
 
17
21
  def self.regex?(pattern)
18
- pattern[0..1] == 'r:'
22
+ pattern[0..1] == 'r/'
19
23
  end
20
24
  private_class_method :regex?
21
25
 
@@ -26,7 +30,9 @@ module CqlRuby
26
30
 
27
31
  def self.regex_match?(pattern, subject)
28
32
  pattern = pattern[2..]
29
- pattern, *mods = pattern.split('+')
33
+ delim_idx = pattern.rindex('/')
34
+ mods = pattern[delim_idx + 1..].chars
35
+ pattern = pattern[0..delim_idx - 1]
30
36
 
31
37
  fops = 0
32
38
  fops |= Regexp::IGNORECASE if mods.include?('i')
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cql_ruby
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.4
4
+ version: 0.0.9
5
5
  platform: ruby
6
6
  authors:
7
7
  - itarato