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