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,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ducalis
4
+ module Adapters
5
+ class CircleCi < Base
6
+ def repo
7
+ @repo ||= ENV.fetch('CIRCLE_REPOSITORY_URL')
8
+ .sub('https://github.com/', '')
9
+ end
10
+
11
+ def id
12
+ @id ||= ENV.fetch('CI_PULL_REQUEST')
13
+ .sub("#{ENV.fetch('CIRCLE_REPOSITORY_URL')}/pull/", '')
14
+ end
15
+
16
+ def sha
17
+ @sha ||= ENV.fetch('CIRCLE_SHA1')
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ducalis
4
+ module Adapters
5
+ class Custom < Base
6
+ def repo
7
+ @repo ||= options.fetch(:repo)
8
+ end
9
+
10
+ def id
11
+ @id ||= options.fetch(:id)
12
+ end
13
+
14
+ def sha
15
+ @sha ||= options.fetch(:sha) do
16
+ Utils.octokit.pull_request(repo, id).head.sha
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ducalis
4
+ module Adapters
5
+ class PullRequest < Base
6
+ def repo
7
+ @repo ||= attributes[:repo]
8
+ end
9
+
10
+ def id
11
+ @id ||= attributes[:number]
12
+ end
13
+
14
+ def sha
15
+ @sha ||= attributes[:head_sha]
16
+ end
17
+
18
+ private
19
+
20
+ def attributes
21
+ @attributes ||= Policial::PullRequestEvent.new(options)
22
+ .pull_request_attributes
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'thor'
4
+
5
+ module Ducalis
6
+ class CLI < Thor
7
+ ADAPTERS = {
8
+ circle: Adapters::CircleCi,
9
+ custom: Adapters::Custom
10
+ }.freeze
11
+ DEFAULT_ADAPTER = ADAPTERS.keys.last
12
+ default_task :start
13
+
14
+ desc '--ci', ''
15
+ option :adapter, required: true, default: DEFAULT_ADAPTER,
16
+ desc: 'Describes how Ducalis will receive PR information'
17
+ option :id, type: :numeric, desc: 'PR id, ex: 2347'
18
+ option :repo, type: :string, desc: 'PR repository, ex: author/repo'
19
+ option :sha, type: :string, desc: 'Starting commit, can be skipped'
20
+ option :dry, type: :boolean, default: false,
21
+ desc: 'Allows user run dry mode, default: false'
22
+ def start
23
+ adapter = ADAPTERS.fetch(options[:adapter].to_sym) do
24
+ raise "Unsupported adapter #{options[:adapter]}"
25
+ end
26
+ Runner.new(adapter.new(options)).call
27
+ end
28
+
29
+ desc '--ci help', 'Describe available commands'
30
+ def help(command = nil, subcommand = false)
31
+ command ||= :start
32
+ super
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'logger'
4
+
5
+ module Ducalis
6
+ module Commentators
7
+ class Console
8
+ DOCUMENTATION_PATH = 'https://github.com/ignat-zakrevsky/ducalis/blob/master/DOCUMENTATION.md'
9
+
10
+ def initialize(config)
11
+ @config = config
12
+ end
13
+
14
+ def call(violations)
15
+ violations.each do |violation|
16
+ logger.info(generate_message(violation))
17
+ end
18
+ end
19
+
20
+ private
21
+
22
+ def generate_message(violation)
23
+ [
24
+ [cyan(violation.filename), violation.line.patch_position].join(':'),
25
+ brown(violation.linter),
26
+ bold(ancor(violation))
27
+ ].join(' ')
28
+ end
29
+
30
+ def logger
31
+ @logger ||= Logger.new(STDOUT).tap do |logger|
32
+ logger.formatter = proc do |_severity, _datetime, _progname, msg|
33
+ "#{msg}\n"
34
+ end
35
+ end
36
+ end
37
+
38
+ def ancor(violation)
39
+ [
40
+ DOCUMENTATION_PATH,
41
+ '#',
42
+ violation.linter.downcase.gsub(/[^[:alpha:]]/, '')
43
+ ].join
44
+ end
45
+
46
+ def bold(text)
47
+ "\e[1m#{text}\e[22m"
48
+ end
49
+
50
+ def brown(text)
51
+ "\e[33m#{text}\e[0m"
52
+ end
53
+
54
+ def cyan(text)
55
+ "\e[36m#{text}\e[0m"
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ducalis
4
+ module Commentators
5
+ class Github
6
+ STATUS = 'COMMENT'
7
+
8
+ def initialize(config)
9
+ @config = config
10
+ end
11
+
12
+ def call(violations)
13
+ comments = violations.map do |violation|
14
+ next if commented?(violation)
15
+ generate_comment(violation)
16
+ end.compact
17
+ Utils.octokit
18
+ .create_pull_request_review(@config.repo, @config.id,
19
+ event: STATUS, comments: comments)
20
+ end
21
+
22
+ private
23
+
24
+ def commented?(violation)
25
+ commented_violations.find do |commented_violation|
26
+ [
27
+ violation.filename == commented_violation[:path],
28
+ violation.line.patch_position == commented_violation[:position],
29
+ Utils.similarity(
30
+ violation.message, commented_violation[:body]
31
+ ) > 0.9
32
+ ].all?
33
+ end
34
+ end
35
+
36
+ def commented_violations
37
+ @commented_violations ||=
38
+ Utils.octokit.pull_request_comments(@config.repo, @config.id)
39
+ end
40
+
41
+ def generate_comment(violation)
42
+ {
43
+ body: violation.message,
44
+ path: violation.filename,
45
+ position: violation.line.patch_position
46
+ }
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rubocop'
4
+
5
+ module Ducalis
6
+ class CallbacksActiverecord < RuboCop::Cop::Cop
7
+ OFFENSE = "Please, avoid using of callbacks for models. It's better to "\
8
+ 'keep models small ("dumb") and instead use "builder" classes'\
9
+ '/services: to construct new objects. You can read more [here]'\
10
+ '(https://medium.com/planet-arkency/a61fd75ab2d3).'
11
+ MODELS_CLASS_NAMES = ['ApplicationRecord', 'ActiveRecord::Base'].freeze
12
+ METHODS_BLACK_LIST = %i[
13
+ after_commit
14
+ after_create
15
+ after_destroy
16
+ after_rollback
17
+ after_save
18
+ after_update
19
+ after_validation
20
+ around_create
21
+ around_destroy
22
+ around_save
23
+ around_update
24
+ before_create
25
+ before_destroy
26
+ before_save
27
+ before_update
28
+ before_validation
29
+ ].freeze
30
+
31
+ def on_class(node)
32
+ _classdef_node, superclass, _body = *node
33
+ @triggered = superclass &&
34
+ MODELS_CLASS_NAMES.include?(superclass.loc.expression.source)
35
+ end
36
+
37
+ def on_send(node)
38
+ return unless @triggered
39
+ return unless METHODS_BLACK_LIST.include?(node.method_name)
40
+ add_offense(node, :selector, OFFENSE)
41
+ end
42
+
43
+ private
44
+
45
+ attr_reader :triggered
46
+ end
47
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rubocop'
4
+
5
+ module Ducalis
6
+ class ControllersExcept < ::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
+ Prefer to use `:only` over `:except` in controllers because it's more explicit \
12
+ and will be easier to maintain for new developers.
13
+ ).strip
14
+
15
+ def on_class(node)
16
+ _classdef_node, superclass, _body = *node
17
+ return if superclass.nil?
18
+ @triggered = superclass.loc.expression.source =~ /Controller/
19
+ end
20
+
21
+ def on_send(node)
22
+ _, method_name, *args = *node
23
+ hash_node = args.find { |subnode| subnode.type == :hash }
24
+ return unless FILTERS.include?(method_name) && hash_node
25
+ type, _method_names = decomposite_hash(hash_node)
26
+ return unless type == s(:sym, :except)
27
+ add_offense(node, :selector, OFFENSE)
28
+ end
29
+
30
+ private
31
+
32
+ def decomposite_hash(args)
33
+ args.to_a.first.children.to_a
34
+ end
35
+
36
+ attr_reader :triggered
37
+ end
38
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rubocop'
4
+
5
+ module Ducalis
6
+ class KeywordDefaults < RuboCop::Cop::Cop
7
+ include RuboCop::Cop::DefNode
8
+ OFFENSE = %(
9
+ Prefer to use keyword arguments for defaults. \
10
+ It increases readability and reduces ambiguities.
11
+ ).strip
12
+
13
+ def on_def(node)
14
+ args = node.type == :defs ? node.to_a[2] : node.to_a[1]
15
+ return unless args
16
+ args.children.each do |arg_node|
17
+ next unless arg_node.type == :optarg
18
+ add_offense(node, :expression, OFFENSE)
19
+ end
20
+ end
21
+ alias on_defs on_def
22
+ end
23
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rubocop'
4
+
5
+ module Ducalis
6
+ class ModuleLikeClass < RuboCop::Cop::Cop
7
+ include RuboCop::Cop::DefNode
8
+
9
+ OFFENSE = %(
10
+ Seems like it will be better to define initialize and pass %<args>s there \
11
+ instead of each method.
12
+ ).strip
13
+
14
+ def on_class(node)
15
+ _name, inheritance, body = *node
16
+ return if !inheritance.nil? || body.nil? || allowed_include?(body)
17
+ matched = matched_args(body)
18
+ return if matched.empty?
19
+ add_offense(node, :expression,
20
+ format(OFFENSE, args:
21
+ matched.map { |arg| "`#{arg}`" }.join(', ')))
22
+ end
23
+
24
+ private
25
+
26
+ def allowed_include?(body)
27
+ return if cop_config['AllowedIncludes'].to_a.empty?
28
+ (all_includes(body) & cop_config['AllowedIncludes']).any?
29
+ end
30
+
31
+ def matched_args(body)
32
+ methods_defintions = children(body).select(&public_method_definition?)
33
+ return [] if methods_defintions.count == 1 && with_initialize?(body)
34
+ methods_defintions.map(&method_args).inject(&:&).to_a
35
+ end
36
+
37
+ def children(body)
38
+ (body.type != :begin ? s(:begin, body) : body).children
39
+ end
40
+
41
+ def all_includes(body)
42
+ children(body).select(&method(:include_node?))
43
+ .map(&:to_a)
44
+ .map { |_, _, node| node.loc.expression.source }
45
+ .to_a
46
+ end
47
+
48
+ def public_method_definition?
49
+ ->(node) { node.type == :def && !non_public?(node) && !initialize?(node) }
50
+ end
51
+
52
+ def method_args
53
+ lambda do |n|
54
+ _name, args = *n
55
+ args.children
56
+ .select { |node| node.type == :arg }
57
+ .map { |node| node.loc.expression.source }
58
+ end
59
+ end
60
+
61
+ def with_initialize?(body)
62
+ children(body).find(&method(:initialize?))
63
+ end
64
+
65
+ def_node_search :include_node?, '(send _ :include (...))'
66
+ def_node_search :initialize?, '(def :initialize ...)'
67
+ end
68
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rubocop'
4
+
5
+ module Ducalis
6
+ class ParamsPassing < RuboCop::Cop::Cop
7
+ include RuboCop::Cop::DefNode
8
+ PARAMS_CALL = s(:send, nil, :params)
9
+ OFFENSE = %(
10
+ It's better to pass already preprocessed params hash to services. Or you can use
11
+ `arcane` gem
12
+ ).strip
13
+
14
+ def on_send(node)
15
+ _who, _what, *args = *node
16
+ node = inspect_args(args)
17
+ add_offense(node, :expression, OFFENSE) if node
18
+ end
19
+
20
+ private
21
+
22
+ def inspect_args(args)
23
+ return if Array(args).empty?
24
+ args.find { |arg| arg == PARAMS_CALL }.tap do |node|
25
+ return node if node
26
+ end
27
+ inspect_hash(args.find { |arg| arg.type == :hash })
28
+ end
29
+
30
+ def inspect_hash(args)
31
+ return if args.nil?
32
+ args.children.find { |arg| arg.to_a[1] == PARAMS_CALL }
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rubocop'
4
+
5
+ module Ducalis
6
+ class PrivateInstanceAssign < RuboCop::Cop::Cop
7
+ include RuboCop::Cop::DefNode
8
+
9
+ OFFENSE = %(
10
+ Please, don't assign instance variables in controller's private methods. It's \
11
+ make hard to understand what variables are available in views.
12
+ ).strip
13
+ ADD_OFFENSE = %(
14
+ If you want to memoize variable, please, add underscore to the variable name \
15
+ start: `@_name`.
16
+ ).strip
17
+ def on_class(node)
18
+ _classdef_node, superclass, _body = *node
19
+ return if superclass.nil?
20
+ @triggered = superclass.loc.expression.source =~ /Controller/
21
+ end
22
+
23
+ def on_ivasgn(node)
24
+ return unless triggered
25
+ return unless non_public?(node)
26
+ return check_memo(node) if node.parent.type == :or_asgn
27
+ add_offense(node, :expression, OFFENSE)
28
+ end
29
+
30
+ private
31
+
32
+ def check_memo(node)
33
+ return if node.to_a.first.to_s.start_with?('@_')
34
+ add_offense(node, :expression, OFFENSE + ADD_OFFENSE)
35
+ end
36
+
37
+ attr_reader :triggered
38
+ end
39
+ end