ducalis 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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