querly 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
  SHA1:
3
- metadata.gz: b38a9eb384827611a39b2d1bef7e1b8cb574f2c8
4
- data.tar.gz: c02c0c2ebb9844465855e2691e02715b5c73b359
3
+ metadata.gz: 23d5bf2430a932df4208453cc4847d06915a5140
4
+ data.tar.gz: 6be0f56ed32a3bded3ef3b9241633faedfc83c3a
5
5
  SHA512:
6
- metadata.gz: e9a5550cec1e87aad9dcfd0372af5a65ec82b46f685c8960c89b7a34316e64ffc2172136a4a238dfa3803349796df235d960ca4ca24df503f8895c4776a9631f
7
- data.tar.gz: fa20ac97b8485b262e625e20a945719c254a7a13f4006e0e8513156567324e3b932c6d75e623b16062085026eb9ad8e4ad902adb169f786026588ebd64cfc729
6
+ metadata.gz: 68bfb69f1c7628a24f2738bf5be828ce7b31d181a82f550ad1ed54b451d7612fa2ff7f4e6a6b6bf229dbfe147c13ff2a0656436c6b2680cfc800bd70106a34c3
7
+ data.tar.gz: 9794d372352bd5cff6704b93604ea6b94942a6e4eaf882602788b0660c579e80bfae6369843b5eb75cb55ae3d97848123e52517b4acbb825d1f961e731acacc0
@@ -0,0 +1,15 @@
1
+ # Change Log
2
+
3
+ ## 0.2.0 (2016-11-24)
4
+
5
+ * Remove `tagging` section from config
6
+ * Add `check` section to select rules to check
7
+ * Add `import` section to load rules from other file
8
+ * Add `querly rules` sub command to print loaded rules
9
+ * Add *with block* and *without block* pattern (`foo {}` / `foo !{}`)
10
+ * Add *some of receiver chain* pattern (`...`)
11
+ * Fix keyword args pattern matching bug
12
+
13
+ ## 0.1.0
14
+
15
+ * First release.
data/README.md CHANGED
@@ -1,22 +1,23 @@
1
- # Querly - Query Method Calls from Ruby Programs
1
+ # Querly - Pattern Based Checking Tool for Ruby
2
2
 
3
3
  [![Build Status](https://travis-ci.org/soutaro/querly.svg?branch=master)](https://travis-ci.org/soutaro/querly)
4
4
 
5
5
  Querly is a query language and tool to find out method calls from Ruby programs.
6
- You write simple query for method calls, and Querly finds out *wrong* pieces from your program.
6
+ Define rules to check your program with patterns to find out *bad* pieces.
7
+ Querly finds out matching pieces from your program.
7
8
 
8
9
  ## Overview
9
10
 
10
- Your project should have many local rules:
11
+ Your project may have many local rules:
11
12
 
12
- * Should not use `Customer#update_mail` but 30x faster `Customer.update_all_email` (Slower `#update_mail` is left just for existing code, but new code should not use it)
13
+ * Should not use `Customer#update_mail` and use 30x faster `Customer.update_all_email` instead (Slower `#update_mail` is left just for existing code, but new code should not use it)
13
14
  * Should not use `root_url` without `locale:` parameter
14
15
  * Should not use `Net::HTTP` for Web API calls, but use `HTTPClient`
15
16
 
16
17
  These local rule violations will be found during code review.
17
- Reviewers will ask commiter to revise, commiter will fix, fine.
18
+ Reviewers will ask commiter to revise; commiter will fix; fine.
18
19
  Really?
19
- It is boring and time consuming.
20
+ It is boring and time-consuming.
20
21
  We need some automation!
21
22
 
22
23
  However, that rules cannot be the standard.
@@ -43,7 +44,7 @@ rules:
43
44
  message: Use HTTPClient to make HTTP request
44
45
  ```
45
46
 
46
- Write down your local rules, and let Querly do boring checks.
47
+ Write down your local rules, and let Querly check conformance with them.
47
48
  Focus on spec, design, UX, and other important things during code review!
48
49
 
49
50
  ## Installation
@@ -66,8 +67,8 @@ Copy the following YAML and paste as `querly.yml` in your project's repo.
66
67
  rules:
67
68
  - id: sample.debug_print
68
69
  pattern:
69
- - p
70
- - pp
70
+ - self.p
71
+ - self.pp
71
72
  message: Delete debug print
72
73
  ```
73
74
 
@@ -120,7 +121,7 @@ There will be many false positives, and *querly warning free code* does not make
120
121
 
121
122
  * TODO: support to ignore warnings through magic comments in code
122
123
 
123
- This is not to ensure *there is nothing wrong in the code*, but just tells you *code fragments you should review with special care*.
124
+ Querly is not to ensure *there is nothing wrong in the code*, but just tells you *code fragments you should review with special care*.
124
125
  I believe it still improves your software development productivity.
125
126
 
126
127
  ### Incoming updates?
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $LOAD_PATH << File.join(__dir__, "../lib")
4
+
5
+ require "querly/pp/cli"
6
+
7
+ Querly::PP::CLI.new(ARGV).run
@@ -18,8 +18,8 @@ require 'querly/node_pair'
18
18
  require "querly/pattern/parser"
19
19
  require 'querly/pattern/kind'
20
20
  require "querly/config"
21
- require "querly/tagging"
22
21
  require "querly/preprocessor"
22
+ require "querly/check"
23
23
 
24
24
  module Querly
25
25
  # Your code goes here...
@@ -1,13 +1,11 @@
1
1
  module Querly
2
2
  class Analyzer
3
- attr_reader :rules
3
+ attr_reader :config
4
4
  attr_reader :scripts
5
- attr_reader :taggings
6
5
 
7
- def initialize(taggings:)
8
- @rules = []
6
+ def initialize(config:)
7
+ @config = config
9
8
  @scripts = []
10
- @taggings = taggings
11
9
  end
12
10
 
13
11
  #
@@ -15,14 +13,11 @@ module Querly
15
13
  #
16
14
  def run
17
15
  scripts.each do |script|
18
- tagging = taggings.find {|tagging| tagging.applicable?(script) }
19
-
20
- each_subnode script.root_pair do |node_pair|
16
+ rules = config.rules_for_path(script.path)
17
+ script.root_pair.each_subpair do |node_pair|
21
18
  rules.each do |rule|
22
- if applicable_rule?(tagging, rule)
23
- if rule.patterns.any? {|pattern| test_pair(node_pair, pattern) }
24
- yield script, rule, node_pair
25
- end
19
+ if rule.patterns.any? {|pattern| test_pair(node_pair, pattern) }
20
+ yield script, rule, node_pair
26
21
  end
27
22
  end
28
23
  end
@@ -31,7 +26,7 @@ module Querly
31
26
 
32
27
  def find(pattern)
33
28
  scripts.each do |script|
34
- each_subnode script.root_pair do |node_pair|
29
+ script.root_pair.each_subpair do |node_pair|
35
30
  if test_pair(node_pair, pattern)
36
31
  yield script, node_pair
37
32
  end
@@ -42,23 +37,5 @@ module Querly
42
37
  def test_pair(node_pair, pattern)
43
38
  pattern.expr =~ node_pair && pattern.test_kind(node_pair)
44
39
  end
45
-
46
- def applicable_rule?(tagging, rule)
47
- if tagging && !rule.tags.empty?
48
- tagging.tags_set.any? {|set| set.subset?(rule.tags) }
49
- else
50
- true
51
- end
52
- end
53
-
54
- def each_subnode(node_pair, &block)
55
- return unless node_pair.node
56
-
57
- yield node_pair
58
-
59
- node_pair.children.each do |child|
60
- each_subnode child, &block
61
- end
62
- end
63
40
  end
64
41
  end
@@ -0,0 +1,104 @@
1
+ module Querly
2
+ class Check
3
+ Query = Struct.new(:opr, :tags, :identifier) do
4
+ def apply(current:, all:)
5
+ case opr
6
+ when :append
7
+ current.union(all.select {|rule| match?(rule) })
8
+ when :except
9
+ current.reject {|rule| match?(rule) }.to_set
10
+ when :only
11
+ all.select {|rule| match?(rule) }.to_set
12
+ end
13
+ end
14
+
15
+ def match?(rule)
16
+ rule.match?(identifier: identifier, tags: tags)
17
+ end
18
+ end
19
+
20
+ attr_reader :patterns
21
+ attr_reader :rules
22
+
23
+ def initialize(pattern:, rules:)
24
+ @rules = rules
25
+
26
+ @has_trailing_slash = pattern.end_with?("/")
27
+ @has_middle_slash = /\/./ =~ pattern
28
+
29
+ @patterns = []
30
+
31
+ pattern.sub!(/\A\//, '')
32
+
33
+ case
34
+ when has_trailing_slash? && has_middle_slash?
35
+ patterns << File.join(pattern, "**")
36
+ when has_trailing_slash?
37
+ patterns << File.join(pattern, "**")
38
+ patterns << File.join("**", pattern, "**")
39
+ when has_middle_slash?
40
+ patterns << pattern
41
+ patterns << File.join(pattern, "**")
42
+ else
43
+ patterns << pattern
44
+ patterns << File.join("**", pattern)
45
+ patterns << File.join(pattern, "**")
46
+ patterns << File.join("**", pattern, "**")
47
+ end
48
+ end
49
+
50
+ def has_trailing_slash?
51
+ @has_trailing_slash
52
+ end
53
+
54
+ def has_middle_slash?
55
+ @has_middle_slash
56
+ end
57
+
58
+ def self.load(hash)
59
+ pattern = hash["path"]
60
+
61
+ rules = Array(hash["rules"]).map do |rule|
62
+ case rule
63
+ when String
64
+ parse_rule_query(:append, rule)
65
+ when Hash
66
+ case
67
+ when rule["append"]
68
+ parse_rule_query(:append, rule["append"])
69
+ when rule["except"]
70
+ parse_rule_query(:except, rule["except"])
71
+ when rule["only"]
72
+ parse_rule_query(:only, rule["only"])
73
+ else
74
+ parse_rule_query(:append, rule)
75
+ end
76
+ end
77
+ end
78
+
79
+ self.new(pattern: pattern, rules: rules)
80
+ end
81
+
82
+ def self.parse_rule_query(opr, query)
83
+ case query
84
+ when String
85
+ Query.new(opr, nil, query)
86
+ when Hash
87
+ if query['tags']
88
+ ts = query['tags']
89
+ if ts.is_a?(String)
90
+ ts = ts.split
91
+ end
92
+ tags = Set.new(ts)
93
+ end
94
+ identifier = query['id']
95
+
96
+ Query.new(opr, tags, identifier)
97
+ end
98
+ end
99
+
100
+ def match?(path:)
101
+ patterns.any? {|pat| File.fnmatch?(pat, path.to_s) }
102
+ end
103
+ end
104
+ end
@@ -5,6 +5,7 @@ module Querly
5
5
  class CLI < Thor
6
6
  desc "check [paths]", "Check paths based on configuration"
7
7
  option :config, default: "querly.yml"
8
+ option :root
8
9
  option :format, default: "text", type: :string, enum: %w(text json)
9
10
  def check(*paths)
10
11
  require 'querly/cli/formatter'
@@ -26,18 +27,20 @@ Specify configuration file by --config option.
26
27
  exit 1
27
28
  end
28
29
 
29
- config = Config.new
30
- begin
31
- config.add_file config_path
30
+ root_option = options[:root]
31
+ root_path = root_option ? Pathname(root_option).realpath : config_path.parent.realpath
32
+
33
+ config = begin
34
+ yaml = YAML.load(config_path.read)
35
+ Config.load(yaml, config_path: config_path, root_dir: root_path, stderr: STDERR)
32
36
  rescue => exn
33
37
  formatter.config_error config_path, exn
34
38
  exit
35
39
  end
36
40
 
37
- analyzer = Analyzer.new(taggings: config.taggings)
38
- analyzer.rules.concat config.rules
41
+ analyzer = Analyzer.new(config: config)
39
42
 
40
- ScriptEnumerator.new(paths: paths.map {|path| Pathname(path) }, preprocessors: config.preprocessors).each do |path, script|
43
+ ScriptEnumerator.new(paths: paths.map {|path| Pathname(path) }, config: config).each do |path, script|
41
44
  case script
42
45
  when Script
43
46
  analyzer.scripts << script
@@ -71,6 +74,13 @@ Specify configuration file by --config option.
71
74
  Test.new(config_path: config_path).run
72
75
  end
73
76
 
77
+ desc "rules", "Print loaded rules"
78
+ option :config, default: "querly.yml"
79
+ def rules(*ids)
80
+ require "querly/cli/rules"
81
+ Rules.new(config_path: config_path, ids: ids).run
82
+ end
83
+
74
84
  private
75
85
 
76
86
  def config_path
@@ -33,9 +33,9 @@ Querly #{VERSION}, interactive console
33
33
  def analyzer
34
34
  return @analyzer if @analyzer
35
35
 
36
- @analyzer = Analyzer.new(taggings: [])
36
+ @analyzer = Analyzer.new(config: nil)
37
37
 
38
- ScriptEnumerator.new(paths: paths).each do |path, script|
38
+ ScriptEnumerator.new(paths: paths, config: nil).each do |path, script|
39
39
  case script
40
40
  when Script
41
41
  @analyzer.scripts << script
@@ -0,0 +1,77 @@
1
+ module Querly
2
+ class CLI
3
+ class Rules
4
+ attr_reader :config_path
5
+ attr_reader :stdout
6
+ attr_reader :ids
7
+
8
+ def initialize(config_path:, ids:, stdout: STDOUT)
9
+ @config_path = config_path
10
+ @stdout = stdout
11
+ @ids = ids
12
+ end
13
+
14
+ def config
15
+ yaml = YAML.load(config_path.read)
16
+ @config ||= Config.load(yaml, config_path: config_path, root_dir: config_path.parent.realpath)
17
+ end
18
+
19
+ def run
20
+ rules = config.rules.select {|rule| test_rule(rule) }
21
+ stdout.puts YAML.dump(rules.map {|rule| rule_to_yaml(rule) })
22
+ end
23
+
24
+ def test_rule(rule)
25
+ if ids.empty?
26
+ true
27
+ else
28
+ ids.any? {|id| rule.match?(identifier: id) }
29
+ end
30
+ end
31
+
32
+ def rule_to_yaml(rule)
33
+ { "id" => rule.id }.tap do |hash|
34
+ singleton rule.sources do |a|
35
+ hash["pattern"] = a
36
+ end
37
+
38
+ singleton rule.messages do |a|
39
+ hash["message"] = a
40
+ end
41
+
42
+ empty rule.tags do |a|
43
+ hash["tags"] = a
44
+ end
45
+
46
+ singleton rule.justifications do |a|
47
+ hash["justification"] = a
48
+ end
49
+
50
+ singleton rule.before_examples do |a|
51
+ hash["before"] = a
52
+ end
53
+
54
+ singleton rule.after_examples do |a|
55
+ hash["after"] = a
56
+ end
57
+ end
58
+ end
59
+
60
+ def empty(array)
61
+ unless array.empty?
62
+ yield array.to_a
63
+ end
64
+ end
65
+
66
+ def singleton(array)
67
+ empty(array) do
68
+ if array.length == 1
69
+ yield array.first
70
+ else
71
+ yield array.to_a
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
@@ -92,12 +92,12 @@ module Querly
92
92
  end
93
93
 
94
94
  def test_pattern(pattern, example, expected:)
95
- analyzer = Analyzer.new(taggings: [])
95
+ analyzer = Analyzer.new(config: nil)
96
96
 
97
97
  found = false
98
98
 
99
99
  node = Parser::CurrentRuby.parse(example)
100
- analyzer.each_subnode NodePair.new(node: node) do |pair|
100
+ NodePair.new(node: node).each_subpair do |pair|
101
101
  if analyzer.test_pair(pair, pattern)
102
102
  found = true
103
103
  end
@@ -108,9 +108,8 @@ module Querly
108
108
 
109
109
  def load_config
110
110
  if config_path.file?
111
- config = Config.new
112
- config.add_file config_path
113
- config
111
+ yaml = YAML.load(config_path.read)
112
+ Config.load(yaml, config_path: config_path, root_dir: config_path.parent.realpath, stderr: STDERR)
114
113
  end
115
114
  end
116
115
  end
@@ -1,55 +1,87 @@
1
1
  module Querly
2
2
  class Config
3
3
  attr_reader :rules
4
- attr_reader :paths
5
- attr_reader :taggings
6
4
  attr_reader :preprocessors
5
+ attr_reader :root_dir
6
+ attr_reader :checks
7
+ attr_reader :rules_cache
7
8
 
8
- def initialize()
9
- @rules = []
10
- @paths = []
11
- @taggings = []
12
- @preprocessors = {}
9
+ def initialize(rules:, preprocessors:, root_dir:, checks:)
10
+ @rules = rules
11
+ @root_dir = root_dir
12
+ @preprocessors = preprocessors
13
+ @checks = checks
14
+ @rules_cache = {}
13
15
  end
14
16
 
15
- def add_file(path)
16
- paths << path
17
+ def self.load(hash, config_path:, root_dir:, stderr: STDERR)
18
+ Factory.new(hash, config_path: config_path, root_dir: root_dir, stderr: stderr).config
19
+ end
17
20
 
18
- content = YAML.load(path.read)
19
- load_rules(content)
20
- load_taggings(content)
21
- load_preprocessors(content["preprocessor"] || {})
21
+ def all_rules
22
+ @all_rules ||= Set.new(rules)
22
23
  end
23
24
 
24
- def load_rules(yaml)
25
- yaml["rules"].each do |hash|
26
- id = hash["id"]
27
- patterns = Array(hash["pattern"]).map {|src| Pattern::Parser.parse(src) }
28
- messages = Array(hash["message"])
29
- justifications = Array(hash["justification"])
30
-
31
- rule = Rule.new(id: id)
32
- rule.patterns.concat patterns
33
- rule.messages.concat messages
34
- rule.justifications.concat justifications
35
- Array(hash["tags"]).each {|tag| rule.tags << tag }
36
- rule.before_examples.concat Array(hash["before"])
37
- rule.after_examples.concat Array(hash["after"])
38
-
39
- rules << rule
40
- end
25
+ def relative_path_from_root(path)
26
+ path.absolute? ? path.relative_path_from(root_dir) : path.cleanpath
41
27
  end
42
28
 
43
- def load_taggings(yaml)
44
- @taggings = Array(yaml["tagging"]).map {|hash|
45
- Tagging.new(path_pattern: hash["path"],
46
- tags_set: Array(hash["tags"]).map {|string| Set.new(string.split) })
47
- }.sort_by {|tagging| -tagging.path_pattern.size }
29
+ def rules_for_path(path)
30
+ relative_path = relative_path_from_root(path)
31
+ matching_checks = checks.select {|check| check.match?(path: relative_path) }
32
+
33
+ if rules_cache.key?(matching_checks)
34
+ rules_cache[matching_checks]
35
+ else
36
+ matching_checks.flat_map(&:rules).inject(all_rules) do |rules, query|
37
+ query.apply(current: rules, all: all_rules)
38
+ end.tap do |rules|
39
+ rules_cache[matching_checks] = rules
40
+ end
41
+ end
48
42
  end
49
43
 
50
- def load_preprocessors(preprocessors)
51
- @preprocessors = preprocessors.each.with_object({}) do |(key, value), hash|
52
- hash[key] = Preprocessor.new(ext: key, command: value)
44
+ class Factory
45
+ attr_reader :yaml
46
+ attr_reader :root_dir
47
+ attr_reader :stderr
48
+ attr_reader :config_path
49
+
50
+ def initialize(yaml, config_path:, root_dir:, stderr: STDERR)
51
+ @yaml = yaml
52
+ @config_path = config_path
53
+ @root_dir = root_dir
54
+ @stderr = stderr
55
+ end
56
+
57
+ def config
58
+ if yaml["tagging"]
59
+ stderr.puts "tagging is deprecated and ignored"
60
+ end
61
+
62
+ rules = Array(yaml["rules"]).map {|hash| Rule.load(hash) }
63
+ preprocessors = (yaml["preprocessor"] || {}).each.with_object({}) do |(key, value), hash|
64
+ hash[key] = Preprocessor.new(ext: key, command: value)
65
+ end
66
+
67
+ imports = Array(yaml["import"])
68
+ imports.each do |import|
69
+ if import["load"]
70
+ load_pattern = Pathname(import["load"])
71
+ load_pattern = config_path.parent + load_pattern if load_pattern.relative?
72
+
73
+ Pathname.glob(load_pattern.to_s) do |path|
74
+ stderr.puts "Loading rules from #{path}..."
75
+ YAML.load(path.read).each do |hash|
76
+ rules << Rule.load(hash)
77
+ end
78
+ end
79
+ end
80
+ end
81
+
82
+ checks = Array(yaml["check"]).map {|hash| Check.load(hash) }
83
+
84
+ Config.new(rules: rules, preprocessors: preprocessors, checks: checks, root_dir: root_dir)
53
85
  end
54
86
  end
55
87
  end
@@ -17,5 +17,19 @@ module Querly
17
17
  end
18
18
  end
19
19
  end
20
+
21
+ def each_subpair(&block)
22
+ if block_given?
23
+ return unless node
24
+
25
+ yield self
26
+
27
+ children.each do |child|
28
+ child.each_subpair &block
29
+ end
30
+ else
31
+ enum_for :each_subpair
32
+ end
33
+ end
20
34
  end
21
35
  end
@@ -128,6 +128,9 @@ module Querly
128
128
  true
129
129
  end
130
130
 
131
+ when :regexp
132
+ type == :regexp
133
+
131
134
  end
132
135
  end
133
136
  end
@@ -136,18 +139,20 @@ module Querly
136
139
  attr_reader :name
137
140
  attr_reader :receiver
138
141
  attr_reader :args
142
+ attr_reader :block
139
143
 
140
- def initialize(receiver:, name:, args: Argument::AnySeq.new)
144
+ def initialize(receiver:, name:, block:, args: Argument::AnySeq.new)
141
145
  @name = name
142
146
  @receiver = receiver
143
147
  @args = args
148
+ @block = block
144
149
  end
145
150
 
146
151
  def =~(pair)
147
152
  # Skip send node with block
148
153
  if pair.node.type == :send && pair.parent
149
154
  if pair.parent.node.type == :block
150
- if pair.parent.node.children.first == pair.node
155
+ if pair.parent.node.children.first.equal? pair.node
151
156
  return false
152
157
  end
153
158
  end
@@ -157,6 +162,9 @@ module Querly
157
162
  end
158
163
 
159
164
  def test_node(node)
165
+ return false if block == true && node.type != :block
166
+ return false if block == false && node.type == :block
167
+
160
168
  node = node.children.first if node&.type == :block
161
169
 
162
170
  case node&.type
@@ -173,15 +181,19 @@ module Querly
173
181
 
174
182
  case args
175
183
  when Argument::AnySeq
176
- if args.tail && first_node
177
- case
178
- when nodes.last.type == :kwsplat
179
- true
180
- when nodes.last.type == :hash && args.tail.is_a?(Argument::KeyValue)
181
- hash = hash_node_to_hash(nodes.last)
182
- test_hash_args(hash, args.tail)
184
+ if args.tail
185
+ if first_node
186
+ case
187
+ when nodes.last.type == :kwsplat
188
+ true
189
+ when nodes.last.type == :hash && args.tail.is_a?(Argument::KeyValue)
190
+ hash = hash_node_to_hash(nodes.last)
191
+ test_hash_args(hash, args.tail)
192
+ else
193
+ test_hash_args({}, args.tail)
194
+ end
183
195
  else
184
- true
196
+ test_hash_args({}, args.tail)
185
197
  end
186
198
  else
187
199
  true
@@ -243,6 +255,22 @@ module Querly
243
255
  end
244
256
  end
245
257
 
258
+ class ReceiverContext < Base
259
+ attr_reader :receiver
260
+
261
+ def initialize(receiver:)
262
+ @receiver = receiver
263
+ end
264
+
265
+ def test_node(node)
266
+ if receiver.test_node(node)
267
+ true
268
+ else
269
+ node&.type == :send && test_node(node.children[0])
270
+ end
271
+ end
272
+ end
273
+
246
274
  class Vcall < Base
247
275
  attr_reader :name
248
276
 
@@ -22,6 +22,7 @@ expr: constant { result = Expr::Constant.new(path: val[0]) }
22
22
  | FLOAT { result = Expr::Literal.new(type: :float, value: val[0]) }
23
23
  | SYMBOL { result = Expr::Literal.new(type: :symbol, value: val[0]) }
24
24
  | NUMBER { result = Expr::Literal.new(type: :number, value: val[0]) }
25
+ | REGEXP { result = Expr::Literal.new(type: :regexp, value: nil) }
25
26
  | DSTR { result = Expr::Dstr.new() }
26
27
  | UNDERBAR { result = Expr::Any.new }
27
28
  | NIL { result = Expr::Nil.new }
@@ -64,14 +65,34 @@ constant: UIDENT { result = [val[0]] }
64
65
  | UIDENT COLONCOLON constant { result = [val[0]] + val[2] }
65
66
 
66
67
  send: LIDENT { result = Expr::Vcall.new(name: val[0]) }
67
- | method_name { result = Expr::Send.new(receiver: Expr::Any.new, name: val[0]) }
68
- | method_name_or_ident LPAREN args RPAREN { result = Expr::Send.new(receiver: Expr::Any.new,
69
- name: val[0],
70
- args: val[2]) }
71
- | expr DOT method_name_or_ident { result = Expr::Send.new(receiver: val[0], name: val[2], args: Argument::AnySeq.new) }
72
- | expr DOT method_name_or_ident LPAREN args RPAREN { result = Expr::Send.new(receiver: val[0],
73
- name: val[2],
74
- args: val[4]) }
68
+ | method_name { result = Expr::Send.new(receiver: Expr::Any.new, name: val[0], block: nil) }
69
+ | method_name_or_ident LPAREN args RPAREN block { result = Expr::Send.new(receiver: Expr::Any.new,
70
+ name: val[0],
71
+ args: val[2],
72
+ block: val[4]) }
73
+ | receiver method_name_or_ident block { result = Expr::Send.new(receiver: val[0],
74
+ name: val[1],
75
+ args: Argument::AnySeq.new,
76
+ block: val[2]) }
77
+ | receiver method_name_or_ident block { result = Expr::Send.new(receiver: val[0],
78
+ name: val[1],
79
+ args: Argument::AnySeq.new,
80
+ block: val[2]) }
81
+ | receiver method_name_or_ident LPAREN args RPAREN block { result = Expr::Send.new(receiver: val[0],
82
+ name: val[1],
83
+ args: val[3],
84
+ block: val[5]) }
85
+ | receiver method_name_or_ident LPAREN args RPAREN block { result = Expr::Send.new(receiver: val[0],
86
+ name: val[1],
87
+ args: val[3],
88
+ block: val[5]) }
89
+
90
+ receiver: expr DOT { result = val[0] }
91
+ | expr DOTDOTDOT { result = Expr::ReceiverContext.new(receiver: val[0]) }
92
+
93
+ block: { result = nil }
94
+ | WITH_BLOCK { result = true }
95
+ | WITHOUT_BLOCK { result = false }
75
96
 
76
97
  end
77
98
 
@@ -116,9 +137,15 @@ def next_token
116
137
  [:SYMBOL, nil]
117
138
  when input.scan(/:number:/)
118
139
  [:NUMBER, nil]
140
+ when input.scan(/:regexp:/)
141
+ [:REGEXP, nil]
119
142
  when input.scan(/:\w+/)
120
143
  s = input.matched
121
144
  [:SYMBOL, s[1, s.size - 1].to_sym]
145
+ when input.scan(/{}/)
146
+ [:WITH_BLOCK, nil]
147
+ when input.scan(/!{}/)
148
+ [:WITHOUT_BLOCK, nil]
122
149
  when input.scan(/[+-]?[0-9]+\.[0-9]/)
123
150
  [:FLOAT, input.matched.to_f]
124
151
  when input.scan(/[+-]?[0-9]+/)
@@ -0,0 +1,74 @@
1
+ require "optparse"
2
+
3
+ module Querly
4
+ module PP
5
+ class CLI
6
+ attr_reader :argv
7
+ attr_reader :command
8
+ attr_reader :load_paths
9
+ attr_reader :requires
10
+
11
+ attr_reader :stdin
12
+ attr_reader :stderr
13
+ attr_reader :stdout
14
+
15
+ def initialize(argv, stdin: STDIN, stdout: STDOUT, stderr: STDERR)
16
+ @argv = argv
17
+ @stdin = stdin
18
+ @stdout = stdout
19
+ @stderr = stderr
20
+
21
+ @load_paths = []
22
+ @requires = []
23
+
24
+ OptionParser.new do |opts|
25
+ opts.banner = "Usage: #{opts.program_name} pp-name [options]"
26
+ opts.on("-I dir") {|path| load_paths << path }
27
+ opts.on("-r lib") {|rq| requires << rq }
28
+ end.permute!(argv)
29
+
30
+ @command = argv.shift&.to_sym
31
+ end
32
+
33
+ def load_libs
34
+ load_paths.each do |path|
35
+ $LOAD_PATH << path
36
+ end
37
+
38
+ requires.each do |lib|
39
+ require lib
40
+ end
41
+
42
+ end
43
+
44
+ def run
45
+ available_commands = [:haml]
46
+
47
+ if available_commands.include?(command)
48
+ send :"run_#{command}"
49
+ else
50
+ stderr.puts "Unknown command: #{command}"
51
+ stderr.puts " available commands: #{available_commands.join(", ")}"
52
+ exit 1
53
+ end
54
+ end
55
+
56
+ def run_haml
57
+ require "haml"
58
+ load_libs
59
+
60
+ source = stdin.read
61
+
62
+ options = Haml::Options.new
63
+
64
+ parser = Haml::Parser.new(source, options)
65
+ parser.parse
66
+
67
+ compiler = Haml::Compiler.new(options)
68
+ compiler.compile(parser.root)
69
+
70
+ stdout.print compiler.precompiled
71
+ end
72
+ end
73
+ end
74
+ end
@@ -2,25 +2,67 @@ module Querly
2
2
  class Rule
3
3
  attr_reader :id
4
4
  attr_reader :patterns
5
-
6
5
  attr_reader :messages
6
+
7
+ attr_reader :sources
7
8
  attr_reader :justifications
8
9
  attr_reader :before_examples
9
10
  attr_reader :after_examples
10
11
  attr_reader :tags
11
- attr_reader :scope
12
12
 
13
- def initialize(id:, scope: :nil)
13
+ def initialize(id:, messages:, patterns:, sources:, tags:, before_examples:, after_examples:, justifications:)
14
14
  @id = id
15
- @scope = scope
16
-
17
- @patterns = []
18
- @messages = []
19
- @justifications = []
20
- @before_examples = []
21
- @after_examples = []
22
- @tags = Set.new
23
- @scope = scope
15
+ @patterns = patterns
16
+ @sources = sources
17
+ @messages = messages
18
+ @justifications = justifications
19
+ @before_examples = before_examples
20
+ @after_examples = after_examples
21
+ @tags = tags
22
+ end
23
+
24
+ def match?(identifier: nil, tags: nil)
25
+ if identifier
26
+ unless id == identifier || id.start_with?(identifier + ".")
27
+ return false
28
+ end
29
+ end
30
+
31
+ if tags
32
+ unless tags.subset?(self.tags)
33
+ return false
34
+ end
35
+ end
36
+
37
+ true
38
+ end
39
+
40
+ class InvalidRuleHashError < StandardError; end
41
+
42
+ def self.load(hash)
43
+ id = hash["id"]
44
+ raise InvalidRuleHashError, "id is missing" unless id
45
+
46
+ srcs = Array(hash["pattern"])
47
+ raise InvalidRuleHashError, "pattern is missing" if srcs.empty?
48
+ patterns = srcs.map {|src| Pattern::Parser.parse(src) }
49
+
50
+ messages = Array(hash["message"])
51
+ raise InvalidRuleHashError, "message is missing" if messages.empty?
52
+
53
+ tags = Set.new(Array(hash["tags"]))
54
+ before_examples = Array(hash["before"])
55
+ after_examples = Array(hash["after"])
56
+ justifications = Array(hash["justification"])
57
+
58
+ Rule.new(id: id,
59
+ messages: messages,
60
+ patterns: patterns,
61
+ sources: srcs,
62
+ tags: tags,
63
+ before_examples: before_examples,
64
+ after_examples: after_examples,
65
+ justifications: justifications)
24
66
  end
25
67
  end
26
68
  end
@@ -1,11 +1,11 @@
1
1
  module Querly
2
2
  class ScriptEnumerator
3
3
  attr_reader :paths
4
- attr_reader :preprocessors
4
+ attr_reader :config
5
5
 
6
- def initialize(paths:, preprocessors: {})
6
+ def initialize(paths:, config:)
7
7
  @paths = paths
8
- @preprocessors = preprocessors
8
+ @config = config
9
9
  end
10
10
 
11
11
  def each(&block)
@@ -54,6 +54,10 @@ module Querly
54
54
  yield(path, script)
55
55
  end
56
56
 
57
+ def preprocessors
58
+ config&.preprocessors || {}
59
+ end
60
+
57
61
  def enumerate_files_in_dir(path, &block)
58
62
  if path.basename.to_s =~ /\A\.[^\.]+/
59
63
  # skip hidden paths
@@ -1,3 +1,3 @@
1
1
  module Querly
2
- VERSION = "0.1.0"
2
+ VERSION = "0.2.0"
3
3
  end
@@ -9,8 +9,8 @@ Gem::Specification.new do |spec|
9
9
  spec.authors = ["Soutaro Matsumoto"]
10
10
  spec.email = ["matsumoto@soutaro.com"]
11
11
 
12
- spec.summary = %q{Query Method Calls from Ruby Programs}
13
- spec.description = %q{Querly is a query language and tool to find out method calls from Ruby programs. You write simple query, and Querly finds out wrong pieces in your program.}
12
+ spec.summary = %q{Pattern Based Checking Tool for Ruby}
13
+ spec.description = %q{Querly is a query language and tool to find out method calls from Ruby programs. Define rules to check your program with patterns to find out *bad* pieces. Querly finds out matching pieces from your program.}
14
14
  spec.homepage = "https://github.com/soutaro/querly"
15
15
 
16
16
  spec.files = `git ls-files -z`
@@ -25,6 +25,7 @@ Gem::Specification.new do |spec|
25
25
  spec.add_development_dependency "rake", "~> 10.0"
26
26
  spec.add_development_dependency "minitest", "~> 5.0"
27
27
  spec.add_development_dependency "racc", "= 1.4.14"
28
+ spec.add_development_dependency "unification_assertion", "0.0.1"
28
29
 
29
30
  spec.add_dependency 'thor', "~> 0.19"
30
31
  spec.add_dependency "parser", "~> 2.3.1"
@@ -118,10 +118,34 @@ rules:
118
118
  tags:
119
119
  - test
120
120
  - capybara
121
+ - id: sample.order-group
122
+ pattern: order...group
123
+ before:
124
+ - records.where.order.group
125
+ - records.order.where.group
126
+ message: |
127
+ Using both group and order may generate broken SQL
128
+
121
129
  preprocessor:
122
130
  .slim: slimrb --compile
123
- tagging:
124
- - path: test
125
- tags:
126
- - path: features/step_definitions
127
- tags: test capybara
131
+
132
+ import:
133
+ - load: rules/*.yml
134
+
135
+ check:
136
+ - path: /
137
+ rules:
138
+ - except: minitest
139
+ - path: /test
140
+ rules:
141
+ - minitest
142
+ - except:
143
+ tags: capybara minitest
144
+ - path: /test/integration
145
+ rules:
146
+ - append:
147
+ tags: capybara minitest
148
+ - path: /features/step_definitions
149
+ rules:
150
+ - append:
151
+ tags: capybara minitest
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: querly
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
  - Soutaro Matsumoto
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2016-10-10 00:00:00.000000000 Z
11
+ date: 2016-11-24 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -66,6 +66,20 @@ dependencies:
66
66
  - - '='
67
67
  - !ruby/object:Gem::Version
68
68
  version: 1.4.14
69
+ - !ruby/object:Gem::Dependency
70
+ name: unification_assertion
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - '='
74
+ - !ruby/object:Gem::Version
75
+ version: 0.0.1
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - '='
81
+ - !ruby/object:Gem::Version
82
+ version: 0.0.1
69
83
  - !ruby/object:Gem::Dependency
70
84
  name: thor
71
85
  requirement: !ruby/object:Gem::Requirement
@@ -109,27 +123,33 @@ dependencies:
109
123
  - !ruby/object:Gem::Version
110
124
  version: '2.1'
111
125
  description: Querly is a query language and tool to find out method calls from Ruby
112
- programs. You write simple query, and Querly finds out wrong pieces in your program.
126
+ programs. Define rules to check your program with patterns to find out *bad* pieces.
127
+ Querly finds out matching pieces from your program.
113
128
  email:
114
129
  - matsumoto@soutaro.com
115
130
  executables:
116
131
  - querly
132
+ - querly-pp
117
133
  extensions: []
118
134
  extra_rdoc_files: []
119
135
  files:
120
136
  - ".gitignore"
121
137
  - ".travis.yml"
138
+ - CHANGELOG.md
122
139
  - Gemfile
123
140
  - README.md
124
141
  - Rakefile
125
142
  - bin/console
126
143
  - bin/setup
127
144
  - exe/querly
145
+ - exe/querly-pp
128
146
  - lib/querly.rb
129
147
  - lib/querly/analyzer.rb
148
+ - lib/querly/check.rb
130
149
  - lib/querly/cli.rb
131
150
  - lib/querly/cli/console.rb
132
151
  - lib/querly/cli/formatter.rb
152
+ - lib/querly/cli/rules.rb
133
153
  - lib/querly/cli/test.rb
134
154
  - lib/querly/config.rb
135
155
  - lib/querly/node_pair.rb
@@ -138,11 +158,11 @@ files:
138
158
  - lib/querly/pattern/kind.rb
139
159
  - lib/querly/pattern/parser.rb
140
160
  - lib/querly/pattern/parser.y
161
+ - lib/querly/pp/cli.rb
141
162
  - lib/querly/preprocessor.rb
142
163
  - lib/querly/rule.rb
143
164
  - lib/querly/script.rb
144
165
  - lib/querly/script_enumerator.rb
145
- - lib/querly/tagging.rb
146
166
  - lib/querly/version.rb
147
167
  - querly.gemspec
148
168
  - sample.yaml
@@ -168,5 +188,5 @@ rubyforge_project:
168
188
  rubygems_version: 2.5.1
169
189
  signing_key:
170
190
  specification_version: 4
171
- summary: Query Method Calls from Ruby Programs
191
+ summary: Pattern Based Checking Tool for Ruby
172
192
  test_files: []
@@ -1,32 +0,0 @@
1
- module Querly
2
- class Tagging
3
- attr_reader :path_pattern
4
- attr_reader :tags_set
5
-
6
- def initialize(path_pattern:, tags_set:)
7
- @path_pattern = path_pattern
8
- @tags_set = tags_set
9
- end
10
-
11
- def applicable?(script)
12
- return true unless path_pattern
13
-
14
- pattern_components = path_pattern.split('/')
15
-
16
- script_path = if script.path.absolute?
17
- script.path
18
- else
19
- script.path.realpath
20
- end
21
- path_components = script_path.to_s.split(File::Separator)
22
-
23
- path_components.each_cons(pattern_components.size) do |slice|
24
- if slice == pattern_components
25
- return true
26
- end
27
- end
28
-
29
- false
30
- end
31
- end
32
- end