ducalis 0.1.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.
Files changed (52) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +11 -0
  3. data/.rspec +2 -0
  4. data/.rubocop.yml +12 -0
  5. data/.ruby-version +1 -0
  6. data/.travis.yml +6 -0
  7. data/DOCUMENTATION.md +556 -0
  8. data/Dockerfile +33 -0
  9. data/Gemfile +21 -0
  10. data/Gemfile.lock +112 -0
  11. data/LICENSE +21 -0
  12. data/README.md +15 -0
  13. data/Rakefile +17 -0
  14. data/bin/ducalis +38 -0
  15. data/bootstrap.sh +9 -0
  16. data/config.ru +18 -0
  17. data/config/.ducalis.yml +57 -0
  18. data/ducalis.gemspec +39 -0
  19. data/lib/ducalis.rb +47 -0
  20. data/lib/ducalis/adapters/base.rb +17 -0
  21. data/lib/ducalis/adapters/circle_ci.rb +21 -0
  22. data/lib/ducalis/adapters/custom.rb +21 -0
  23. data/lib/ducalis/adapters/pull_request.rb +26 -0
  24. data/lib/ducalis/cli.rb +35 -0
  25. data/lib/ducalis/commentators/console.rb +59 -0
  26. data/lib/ducalis/commentators/github.rb +50 -0
  27. data/lib/ducalis/cops/callbacks_activerecord.rb +47 -0
  28. data/lib/ducalis/cops/controllers_except.rb +38 -0
  29. data/lib/ducalis/cops/keyword_defaults.rb +23 -0
  30. data/lib/ducalis/cops/module_like_class.rb +68 -0
  31. data/lib/ducalis/cops/params_passing.rb +35 -0
  32. data/lib/ducalis/cops/private_instance_assign.rb +39 -0
  33. data/lib/ducalis/cops/protected_scope_cop.rb +38 -0
  34. data/lib/ducalis/cops/raise_withour_error_class.rb +20 -0
  35. data/lib/ducalis/cops/regex_cop.rb +52 -0
  36. data/lib/ducalis/cops/rest_only_cop.rb +33 -0
  37. data/lib/ducalis/cops/rubocop_disable.rb +19 -0
  38. data/lib/ducalis/cops/strings_in_activerecords.rb +39 -0
  39. data/lib/ducalis/cops/uncommented_gem.rb +33 -0
  40. data/lib/ducalis/cops/useless_only.rb +50 -0
  41. data/lib/ducalis/documentation.rb +101 -0
  42. data/lib/ducalis/passed_args.rb +22 -0
  43. data/lib/ducalis/patched_rubocop/diffs.rb +30 -0
  44. data/lib/ducalis/patched_rubocop/ducalis_config_loader.rb +14 -0
  45. data/lib/ducalis/patched_rubocop/git_files_access.rb +42 -0
  46. data/lib/ducalis/patched_rubocop/git_runner.rb +14 -0
  47. data/lib/ducalis/patched_rubocop/git_turget_finder.rb +11 -0
  48. data/lib/ducalis/patched_rubocop/rubo_cop.rb +32 -0
  49. data/lib/ducalis/runner.rb +48 -0
  50. data/lib/ducalis/utils.rb +27 -0
  51. data/lib/ducalis/version.rb +5 -0
  52. metadata +201 -0
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rubocop'
4
+
5
+ module Ducalis
6
+ class ProtectedScopeCop < RuboCop::Cop::Cop
7
+ OFFENSE = %{
8
+ Seems like you are using `find` on non-protected scope. Potentially it could
9
+ lead to unauthorized access. It's better to call `find` on authorized resources
10
+ scopes. Example:
11
+
12
+ ```ruby
13
+ current_group.employees.find(params[:id])
14
+ # better then
15
+ Employee.find(params[:id])
16
+ ```
17
+ }.strip
18
+
19
+ def on_send(node)
20
+ _, method_name, = *node
21
+ return unless method_name == :find
22
+ return unless children(node).any? { |subnode| subnode.type == :const }
23
+ add_offense(node, :expression, OFFENSE)
24
+ end
25
+
26
+ private
27
+
28
+ def children(node)
29
+ current_nodes = [node]
30
+ while current_nodes.any? { |subnode| subnode.child_nodes.count != 0 }
31
+ current_nodes = current_nodes.flat_map do |subnode|
32
+ subnode.child_nodes.count.zero? ? subnode : subnode.child_nodes
33
+ end
34
+ end
35
+ current_nodes
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rubocop'
4
+
5
+ module Ducalis
6
+ class RaiseWithourErrorClass < RuboCop::Cop::Cop
7
+ include RuboCop::Cop::DefNode
8
+ OFFENSE = %(
9
+ It's better to add exception class as raise argument. It will make easier to \
10
+ catch and process it later.
11
+ ).strip
12
+
13
+ def on_send(node)
14
+ _who, what, *args = *node
15
+ return if what != :raise
16
+ return if args.first.type != :str
17
+ add_offense(node, :expression, OFFENSE)
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rubocop'
4
+ require 'regexp-examples'
5
+
6
+ module Ducalis
7
+ class RegexCop < RuboCop::Cop::Cop
8
+ OFFENSE = %(
9
+ It's better to move regex to constants with example instead of direct using it.
10
+ It will allow you to reuse this regex and provide instructions for others.
11
+
12
+ ```ruby
13
+ CONST_NAME = %<constant>s # "%<example>s"
14
+ %<fixed_string>s
15
+ ```
16
+ ).strip
17
+ SELF_DESCRIPTIVE = %w(
18
+ /[[:alnum:]]/
19
+ /[[:alpha:]]/
20
+ /[[:blank:]]/
21
+ /[[:cntrl:]]/
22
+ /[[:digit:]]/
23
+ /[[:graph:]]/
24
+ /[[:lower:]]/
25
+ /[[:print:]]/
26
+ /[[:punct:]]/
27
+ /[[:space:]]/
28
+ /[[:upper:]]/
29
+ /[[:xdigit:]]/
30
+ /[[:word:]]/
31
+ /[[:ascii:]]/
32
+ ).freeze
33
+
34
+ def on_regexp(node)
35
+ return if node.parent.type == :casgn
36
+ return if SELF_DESCRIPTIVE.include?(node.source)
37
+ return if node.child_nodes.any? { |child_node| child_node.type == :begin }
38
+ add_offense(node, :expression, format(OFFENSE, present_node(node)))
39
+ end
40
+
41
+ private
42
+
43
+ def present_node(node)
44
+ {
45
+ constant: node.source,
46
+ fixed_string: node.source_range.source_line
47
+ .sub(node.source, 'CONST_NAME').lstrip,
48
+ example: Regexp.new(node.to_a.first.to_a.first).examples.sample
49
+ }
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rubocop'
4
+
5
+ module Ducalis
6
+ class RestOnlyCop < RuboCop::Cop::Cop
7
+ include RuboCop::Cop::DefNode
8
+ WHITELIST = %i[index show new edit create update destroy].freeze
9
+ OFFENSE = %(
10
+ It's better for controllers to stay adherent to REST:
11
+ http://jeromedalbert.com/how-dhh-organizes-his-rails-controllers/
12
+ ).strip
13
+
14
+ def on_class(node)
15
+ _classdef_node, superclass, _body = *node
16
+ return if superclass.nil?
17
+ @triggered = superclass.loc.expression.source =~ /Controller/
18
+ end
19
+
20
+ def on_def(node)
21
+ return unless triggered
22
+ return if non_public?(node)
23
+ method_name, = *node
24
+ return if WHITELIST.include?(method_name)
25
+ add_offense(node, :expression, OFFENSE)
26
+ end
27
+ alias on_defs on_def
28
+
29
+ private
30
+
31
+ attr_reader :triggered
32
+ end
33
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rubocop'
4
+
5
+ module Ducalis
6
+ class RubocopDisable < RuboCop::Cop::Cop
7
+ OFFENSE = %(
8
+ Please, do not suppress RuboCop metrics, may be you can introduce some \
9
+ refactoring or another concept.
10
+ )
11
+ def investigate(processed_source)
12
+ return unless processed_source.ast
13
+ processed_source.comments.each do |comment_node|
14
+ next unless comment_node.loc.expression.source.match?(/rubocop:disable/)
15
+ add_offense(comment_node, :expression, OFFENSE)
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rubocop'
4
+ require_relative './callbacks_activerecord'
5
+
6
+ module Ducalis
7
+ class StringsInActiverecords < ::RuboCop::Cop::Cop
8
+ OFFENSE = %(
9
+ Please, do not use strings as arguments for %<method_name>s argument.
10
+ It's hard to test, grep sources, code highlighting and so on.
11
+ Consider using of symbols or lambdas for complex expressions.
12
+ ).strip
13
+ VALIDATEBLE_METHODS =
14
+ ::Ducalis::CallbacksActiverecord::METHODS_BLACK_LIST + %i[
15
+ validates
16
+ validate
17
+ ]
18
+
19
+ def on_send(node)
20
+ _, method_name, *args = *node
21
+ return unless VALIDATEBLE_METHODS.include?(method_name)
22
+ return if args.empty?
23
+ node.to_a.last.each_child_node do |current_node|
24
+ next if skip_node?(current_node)
25
+ add_offense(node, :selector, format(OFFENSE, method_name: method_name))
26
+ end
27
+ end
28
+
29
+ private
30
+
31
+ def skip_node?(current_node)
32
+ key, value = *current_node
33
+ return true unless current_node.type == :pair
34
+ return true unless %w[if unless].include?(key.source)
35
+ return true unless value.type == :str
36
+ false
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rubocop'
4
+
5
+ module Ducalis
6
+ class UncommentedGem < ::RuboCop::Cop::Cop
7
+ OFFENSE = %(
8
+ Please, add comment why are you including non-realized gem version for %<gem>s.
9
+ It will increase [bus-factor](<https://en.wikipedia.org/wiki/Bus_factor>).
10
+ ).strip
11
+
12
+ def investigate(processed_source)
13
+ return unless processed_source.ast
14
+ gem_declarations(processed_source.ast).select do |node|
15
+ _, _, gemname, args = *node
16
+ next if args.nil? || args.type == :str
17
+ next if commented?(processed_source, node)
18
+ add_offense(node, :selector,
19
+ format(OFFENSE, gem: gemname.loc.expression.source))
20
+ end
21
+ end
22
+
23
+ private
24
+
25
+ def_node_search :gem_declarations, '(send nil :gem str ...)'
26
+
27
+ def commented?(processed_source, node)
28
+ processed_source.comments
29
+ .map { |subnode| subnode.loc.line }
30
+ .include?(node.loc.line)
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rubocop'
4
+
5
+ module Ducalis
6
+ class UselessOnly < RuboCop::Cop::Cop
7
+ include RuboCop::Cop::DefNode
8
+ FILTERS = %i[before_filter after_filter around_filter
9
+ before_action after_action around_action].freeze
10
+ OFFENSE = %(
11
+ Seems like there is no any reason to keep before filter only for one action. \
12
+ Maybe it will be better to inline it?
13
+
14
+ ```ruby
15
+ before_filter :do_something, only: %i[index]
16
+ def index; end
17
+
18
+ # to
19
+
20
+ def index
21
+ do_something
22
+ end
23
+ ```
24
+ ).strip
25
+
26
+ def on_class(node)
27
+ _classdef_node, superclass, _body = *node
28
+ return if superclass.nil?
29
+ @triggered = superclass.loc.expression.source =~ /Controller/
30
+ end
31
+
32
+ def on_send(node)
33
+ _, method_name, *args = *node
34
+ hash_node = args.find { |subnode| subnode.type == :hash }
35
+ return unless FILTERS.include?(method_name) && hash_node
36
+ type, method_names = decomposite_hash(hash_node)
37
+ return unless type == s(:sym, :only)
38
+ return unless method_names.children.count == 1
39
+ add_offense(node, :selector, OFFENSE)
40
+ end
41
+
42
+ private
43
+
44
+ def decomposite_hash(args)
45
+ args.to_a.first.children.to_a
46
+ end
47
+
48
+ attr_reader :triggered
49
+ end
50
+ end
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'parser/current'
4
+
5
+ class SpecsProcessor < Parser::AST::Processor
6
+ attr_reader :cases
7
+
8
+ LINE_BEGIN_OPEN_SQUARE_BRACKET = /\A\[/ # "/[/1, 2, 3]\n"
9
+ CLOSE_SQUARE_BRACKET_END_LINE = /\]\z/ # "[1, 2, 3/]\n/"
10
+ LINE_BEGIN_QUOTE = /\A[\'|\"]/ # "/'/idddqd',"
11
+ QUOTE_COMMA_END_LINE = /[\'|\"]\,?\z/ # "'iddqd/',/"
12
+
13
+ def initialize(*)
14
+ super
15
+ @cases = []
16
+ @nesting = []
17
+ end
18
+
19
+ def process(node)
20
+ @nesting.push(node)
21
+ super
22
+ @nesting.pop
23
+ end
24
+
25
+ def on_send(node)
26
+ _, name, _body = *node
27
+ if name == :inspect_source
28
+ source_code = remove_array_wrapping(node.to_a.last.loc.expression.source)
29
+ .split("\n")
30
+ .map { |line| remove_string_wrapping(line) }
31
+ cases << [current_it, source_code]
32
+ end
33
+ super
34
+ end
35
+
36
+ private
37
+
38
+ def current_it
39
+ it_block = @nesting.reverse.find { |node| node.type == :block }
40
+ it = it_block.to_a.first
41
+ _, _, message_node = *it
42
+ message_node.to_a.first
43
+ end
44
+
45
+ def remove_array_wrapping(source)
46
+ source.sub(LINE_BEGIN_OPEN_SQUARE_BRACKET, '')
47
+ .sub(CLOSE_SQUARE_BRACKET_END_LINE, '')
48
+ end
49
+
50
+ def remove_string_wrapping(line)
51
+ line.strip.sub(LINE_BEGIN_QUOTE, '')
52
+ .sub(QUOTE_COMMA_END_LINE, '')
53
+ end
54
+ end
55
+
56
+ class Documentation
57
+ def call
58
+ Dir['./lib/ducalis/cops/*.rb'].map do |f|
59
+ present_cop(klass_const_for(f), spec_cases_for(f))
60
+ end.flatten.join("\n")
61
+ end
62
+
63
+ private
64
+
65
+ def present_cop(klass, specs)
66
+ [
67
+ "## #{klass}\n", # header
68
+ klass.const_get(:OFFENSE) # description
69
+ ] +
70
+ specs.map do |(it, code)|
71
+ [
72
+ "- #{it}", # case description
73
+ "```ruby\n#{code.join("\n")}\n```" # code example
74
+ ]
75
+ end
76
+ end
77
+
78
+ def spec_cases_for(f)
79
+ source_code = File.read(
80
+ f.sub('/lib/ducalis/', '/spec/')
81
+ .sub(/.rb$/, '_spec.rb')
82
+ )
83
+ SpecsProcessor.new.tap do |processor|
84
+ processor.process(Parser::CurrentRuby.parse(source_code))
85
+ end.cases
86
+ end
87
+
88
+ def klass_const_for(f)
89
+ require f
90
+ Ducalis.const_get(camelize(File.basename(f).sub(/.rb$/, '')))
91
+ end
92
+
93
+ def camelize(snake_case_word)
94
+ snake_case_word.sub(/^[a-z\d]*/, &:capitalize).tap do |string|
95
+ string.gsub!(%r{(?:_|(/))([a-z\d]*)}i) do
96
+ "#{Regexp.last_match(1)}#{Regexp.last_match(2).capitalize}"
97
+ end
98
+ string.gsub!('/', '::')
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ducalis
4
+ module PassedArgs
5
+ module_function
6
+
7
+ def help_command?
8
+ ARGV.any? { |arg| Thor::HELP_MAPPINGS.include?(arg) }
9
+ end
10
+
11
+ def ci_mode?
12
+ ARGV.any? { |arg| arg == '--ci' }
13
+ end
14
+
15
+ def process_args!
16
+ flag = PatchedRubocop::MODES.keys
17
+ .map { |key| key if ARGV.delete("--#{key}") }
18
+ .find { |possible_flag| !possible_flag.nil? }
19
+ PatchedRubocop.configure!(flag || :all)
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PatchedRubocop
4
+ module Diffs
5
+ class BaseDiff
6
+ attr_reader :full_path, :diff
7
+
8
+ def initialize(full_path, diff)
9
+ @full_path = full_path
10
+ @diff = diff
11
+ end
12
+ end
13
+
14
+ class NilDiff < BaseDiff
15
+ def changed?(*)
16
+ true
17
+ end
18
+ end
19
+
20
+ class GitDiff < BaseDiff
21
+ def changed?(changed_line)
22
+ (Policial::Patch.new(diff.patch).changed_lines.detect do |line|
23
+ line.number == changed_line
24
+ end || Policial::UnchangedLine.new).changed?
25
+ end
26
+ end
27
+
28
+ private_constant :BaseDiff, :NilDiff, :GitDiff
29
+ end
30
+ end