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