ducalis 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +11 -0
- data/.rspec +2 -0
- data/.rubocop.yml +12 -0
- data/.ruby-version +1 -0
- data/.travis.yml +6 -0
- data/DOCUMENTATION.md +556 -0
- data/Dockerfile +33 -0
- data/Gemfile +21 -0
- data/Gemfile.lock +112 -0
- data/LICENSE +21 -0
- data/README.md +15 -0
- data/Rakefile +17 -0
- data/bin/ducalis +38 -0
- data/bootstrap.sh +9 -0
- data/config.ru +18 -0
- data/config/.ducalis.yml +57 -0
- data/ducalis.gemspec +39 -0
- data/lib/ducalis.rb +47 -0
- data/lib/ducalis/adapters/base.rb +17 -0
- data/lib/ducalis/adapters/circle_ci.rb +21 -0
- data/lib/ducalis/adapters/custom.rb +21 -0
- data/lib/ducalis/adapters/pull_request.rb +26 -0
- data/lib/ducalis/cli.rb +35 -0
- data/lib/ducalis/commentators/console.rb +59 -0
- data/lib/ducalis/commentators/github.rb +50 -0
- data/lib/ducalis/cops/callbacks_activerecord.rb +47 -0
- data/lib/ducalis/cops/controllers_except.rb +38 -0
- data/lib/ducalis/cops/keyword_defaults.rb +23 -0
- data/lib/ducalis/cops/module_like_class.rb +68 -0
- data/lib/ducalis/cops/params_passing.rb +35 -0
- data/lib/ducalis/cops/private_instance_assign.rb +39 -0
- data/lib/ducalis/cops/protected_scope_cop.rb +38 -0
- data/lib/ducalis/cops/raise_withour_error_class.rb +20 -0
- data/lib/ducalis/cops/regex_cop.rb +52 -0
- data/lib/ducalis/cops/rest_only_cop.rb +33 -0
- data/lib/ducalis/cops/rubocop_disable.rb +19 -0
- data/lib/ducalis/cops/strings_in_activerecords.rb +39 -0
- data/lib/ducalis/cops/uncommented_gem.rb +33 -0
- data/lib/ducalis/cops/useless_only.rb +50 -0
- data/lib/ducalis/documentation.rb +101 -0
- data/lib/ducalis/passed_args.rb +22 -0
- data/lib/ducalis/patched_rubocop/diffs.rb +30 -0
- data/lib/ducalis/patched_rubocop/ducalis_config_loader.rb +14 -0
- data/lib/ducalis/patched_rubocop/git_files_access.rb +42 -0
- data/lib/ducalis/patched_rubocop/git_runner.rb +14 -0
- data/lib/ducalis/patched_rubocop/git_turget_finder.rb +11 -0
- data/lib/ducalis/patched_rubocop/rubo_cop.rb +32 -0
- data/lib/ducalis/runner.rb +48 -0
- data/lib/ducalis/utils.rb +27 -0
- data/lib/ducalis/version.rb +5 -0
- 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
|
data/lib/ducalis/cli.rb
ADDED
@@ -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
|