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,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