mudguard 0.1.4 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +8 -0
- data/.mudguard.yml +6 -0
- data/.rubocop.yml +2 -0
- data/Gemfile.lock +25 -23
- data/Guardfile +1 -1
- data/README.md +62 -9
- data/exe/mudguard +5 -12
- data/lib/mudguard.rb +2 -17
- data/lib/mudguard/application/application.rb +33 -0
- data/lib/mudguard/domain/const_visitor.rb +20 -0
- data/lib/mudguard/domain/consts.rb +81 -0
- data/lib/mudguard/domain/dependencies.rb +47 -0
- data/lib/mudguard/domain/dependency.rb +28 -0
- data/lib/mudguard/domain/dependency_visitor.rb +32 -0
- data/lib/mudguard/{error.rb → domain/error.rb} +3 -1
- data/lib/mudguard/domain/policies.rb +49 -0
- data/lib/mudguard/domain/source.rb +79 -0
- data/lib/mudguard/domain/source_policies.rb +8 -0
- data/lib/mudguard/domain/source_processor.rb +92 -0
- data/lib/mudguard/domain/texts.rb +42 -0
- data/lib/mudguard/infrastructure/cli/controller.rb +78 -0
- data/lib/mudguard/infrastructure/cli/notification_adapter.rb +26 -0
- data/lib/mudguard/infrastructure/cli/view.rb +14 -0
- data/lib/mudguard/infrastructure/persistence/.mudguard.template.yml +6 -0
- data/lib/mudguard/infrastructure/persistence/policy_file.rb +46 -0
- data/lib/mudguard/infrastructure/persistence/project_repository.rb +31 -0
- data/lib/mudguard/infrastructure/persistence/ruby_files.rb +39 -0
- data/lib/mudguard/infrastructure/rake/task.rb +28 -0
- data/lib/mudguard/version.rb +1 -1
- data/mudguard.gemspec +2 -1
- metadata +40 -11
- data/lib/mudguard/policies.rb +0 -23
- data/lib/mudguard/policy_file.rb +0 -17
- data/lib/mudguard/ruby_analyser.rb +0 -74
- data/lib/mudguard/ruby_files.rb +0 -17
- data/lib/tasks/gem.rake +0 -15
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Mudguard
|
4
|
+
module Domain
|
5
|
+
# A Dependency between Modules
|
6
|
+
class Dependency
|
7
|
+
attr_reader :location, :dependency
|
8
|
+
|
9
|
+
def initialize(location: nil, dependency:)
|
10
|
+
@location = location
|
11
|
+
@dependency = dependency
|
12
|
+
end
|
13
|
+
|
14
|
+
def inspect
|
15
|
+
"{#{@location}, #{@dependency}}"
|
16
|
+
end
|
17
|
+
|
18
|
+
def match(policy)
|
19
|
+
@dependency.match(policy)
|
20
|
+
end
|
21
|
+
|
22
|
+
def ==(other)
|
23
|
+
@location == other.instance_eval { @location } &&
|
24
|
+
@dependency == other.instance_eval { @dependency }
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Mudguard
|
4
|
+
module Domain
|
5
|
+
# Transforms AST-Nodes into Dependencies
|
6
|
+
class DependencyVisitor
|
7
|
+
def initialize(consts:)
|
8
|
+
@consts = consts
|
9
|
+
@dependencies = []
|
10
|
+
end
|
11
|
+
|
12
|
+
attr_reader :dependencies
|
13
|
+
|
14
|
+
def visit_dependency(location, const_name, module_name)
|
15
|
+
qualified_const_name = @consts.resolve(module_name, const_name)
|
16
|
+
return [] unless qualified_const_name&.include?("::")
|
17
|
+
|
18
|
+
dependency = if module_name.empty?
|
19
|
+
"->#{qualified_const_name}"
|
20
|
+
else
|
21
|
+
"#{module_name}->#{qualified_const_name}"
|
22
|
+
end
|
23
|
+
|
24
|
+
@dependencies << Dependency.new(location: location, dependency: dependency)
|
25
|
+
end
|
26
|
+
|
27
|
+
# rubocop:disable Naming/MethodParameterName
|
28
|
+
def visit_const_declaration(_, __, ___); end
|
29
|
+
# rubocop:enable Naming/MethodParameterName
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "dependencies"
|
4
|
+
require_relative "texts"
|
5
|
+
require_relative "consts"
|
6
|
+
|
7
|
+
module Mudguard
|
8
|
+
module Domain
|
9
|
+
# Contains the policies to be enforced
|
10
|
+
class Policies
|
11
|
+
include Texts
|
12
|
+
|
13
|
+
def initialize(source_policies: [])
|
14
|
+
@source_policies = source_policies
|
15
|
+
end
|
16
|
+
|
17
|
+
def check(notification)
|
18
|
+
result = analyse(:check, notification)
|
19
|
+
|
20
|
+
count = result[:sources_count]
|
21
|
+
violations = result[:analyser_count]
|
22
|
+
|
23
|
+
notification.add(nil, summary(count, violations))
|
24
|
+
violations.zero?
|
25
|
+
end
|
26
|
+
|
27
|
+
def print_allowed_dependencies(notification)
|
28
|
+
result = analyse(:print_allowed, notification)
|
29
|
+
|
30
|
+
count = result[:sources_count]
|
31
|
+
violations = result[:analyser_count]
|
32
|
+
|
33
|
+
notification.add(nil, dependency_summary(count, violations))
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def analyse(method, notification)
|
39
|
+
consts = Consts.new(sources: @source_policies.map(&:source))
|
40
|
+
@source_policies.each_with_object(sources_count: 0, analyser_count: 0) do |sp, result|
|
41
|
+
analyser = Dependencies.new(policies: sp.policies, notification: notification)
|
42
|
+
dependencies = sp.source.find_mod_dependencies(consts)
|
43
|
+
result[:sources_count] += 1
|
44
|
+
result[:analyser_count] += analyser.send(method, dependencies)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "parser/current"
|
4
|
+
require_relative "dependency"
|
5
|
+
require_relative "dependency_visitor"
|
6
|
+
require_relative "const_visitor"
|
7
|
+
require_relative "error"
|
8
|
+
require_relative "source_processor"
|
9
|
+
|
10
|
+
module Mudguard
|
11
|
+
module Domain
|
12
|
+
# Represents a Ruby source file
|
13
|
+
class Source
|
14
|
+
def initialize(location:, code_loader: -> { "" })
|
15
|
+
@code_loader = code_loader
|
16
|
+
@location = location
|
17
|
+
end
|
18
|
+
|
19
|
+
def ==(other)
|
20
|
+
@location == other.instance_eval { @location }
|
21
|
+
end
|
22
|
+
|
23
|
+
def hash
|
24
|
+
@location.hash
|
25
|
+
end
|
26
|
+
|
27
|
+
def eql?(other)
|
28
|
+
@location.eql?(other.instance_eval { @location })
|
29
|
+
end
|
30
|
+
|
31
|
+
def inspect
|
32
|
+
@location
|
33
|
+
end
|
34
|
+
|
35
|
+
def find_mod_dependencies(consts)
|
36
|
+
visitor = DependencyVisitor.new(consts: consts)
|
37
|
+
visit_ast(visitor)
|
38
|
+
visitor.dependencies
|
39
|
+
end
|
40
|
+
|
41
|
+
def find_consts
|
42
|
+
visitor = ConstVisitor.new
|
43
|
+
visit_ast(visitor)
|
44
|
+
visitor.consts
|
45
|
+
end
|
46
|
+
|
47
|
+
def location?(glob)
|
48
|
+
@location == glob
|
49
|
+
end
|
50
|
+
|
51
|
+
private
|
52
|
+
|
53
|
+
SYNTAX_ERROR = "error"
|
54
|
+
|
55
|
+
def ast
|
56
|
+
@ast ||= create_ast
|
57
|
+
end
|
58
|
+
|
59
|
+
def create_ast
|
60
|
+
begin
|
61
|
+
root = Parser::CurrentRuby.parse(code)
|
62
|
+
rescue Parser::SyntaxError
|
63
|
+
return SYNTAX_ERROR
|
64
|
+
end
|
65
|
+
root.nil? ? SYNTAX_ERROR : root
|
66
|
+
end
|
67
|
+
|
68
|
+
def code
|
69
|
+
@code ||= @code_loader.call
|
70
|
+
end
|
71
|
+
|
72
|
+
def visit_ast(visitor)
|
73
|
+
return if ast == SYNTAX_ERROR
|
74
|
+
|
75
|
+
SourceProcessor.new(location: @location).process(ast, visitor)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,92 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Mudguard
|
4
|
+
module Domain
|
5
|
+
# Processes the interesting parts of the Ast and forwards selected nodes to the visitors
|
6
|
+
class SourceProcessor
|
7
|
+
def initialize(location:)
|
8
|
+
@location = location
|
9
|
+
end
|
10
|
+
|
11
|
+
def process(node, visitor)
|
12
|
+
process_node(node, visitor, "")
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def process_node(node, visitor, module_name) # rubocop:disable Metrics/MethodLength
|
18
|
+
case node
|
19
|
+
when type?(:module)
|
20
|
+
process_module(node, visitor, module_name)
|
21
|
+
when type?(:class)
|
22
|
+
process_module(node, visitor, module_name)
|
23
|
+
when type?(:const)
|
24
|
+
process_const(node, visitor, module_name)
|
25
|
+
when type?(:casgn)
|
26
|
+
process_const_assignment(node, visitor, module_name)
|
27
|
+
else
|
28
|
+
ignore_and_continue(node, visitor, module_name)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def process_const_assignment(node, visitor, module_name)
|
33
|
+
is_explicit, is_static, const_name = find_const_name(node.children)
|
34
|
+
return unless is_static
|
35
|
+
|
36
|
+
visitor.visit_const_declaration(describe_location(node), const_name,
|
37
|
+
is_explicit ? "" : module_name)
|
38
|
+
end
|
39
|
+
|
40
|
+
def process_const(node, visitor, module_name)
|
41
|
+
is_explicit, is_static, const_name = find_const_name(node.children)
|
42
|
+
return unless is_static
|
43
|
+
|
44
|
+
visitor.visit_dependency(describe_location(node), const_name,
|
45
|
+
is_explicit ? "" : module_name)
|
46
|
+
end
|
47
|
+
|
48
|
+
def describe_location(node)
|
49
|
+
"#{@location}:#{node.location.line}"
|
50
|
+
end
|
51
|
+
|
52
|
+
def ignore_and_continue(node, visitor, module_name)
|
53
|
+
return unless node.respond_to?(:children)
|
54
|
+
|
55
|
+
node.children.flat_map { |c| process_node(c, visitor, module_name) }
|
56
|
+
end
|
57
|
+
|
58
|
+
def process_module(node, visitor, module_name)
|
59
|
+
is_explicit, is_static, const_name = find_const_name(node.children[0].children)
|
60
|
+
return unless is_static
|
61
|
+
|
62
|
+
visitor.visit_const_declaration(describe_location(node), const_name, module_name)
|
63
|
+
|
64
|
+
module_name = "#{is_explicit ? '' : module_name}#{const_name}"
|
65
|
+
node.children.drop(1).reject(&:nil?).each do |child_node|
|
66
|
+
process_node(child_node, visitor, module_name)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def find_const_name(children)
|
71
|
+
return [false, nil] if children.nil? || children.empty?
|
72
|
+
|
73
|
+
first_child = children[0]
|
74
|
+
is_explicit, is_static = find_const_type(first_child)
|
75
|
+
first_child_children = first_child.respond_to?(:children) ? first_child.children : nil
|
76
|
+
_, __, module_name = find_const_name(first_child_children)
|
77
|
+
const_name = children[1].to_s
|
78
|
+
[is_explicit, is_static, "#{module_name}::#{const_name}"]
|
79
|
+
end
|
80
|
+
|
81
|
+
def find_const_type(first_child)
|
82
|
+
is_explicit = type?(:cbase).call(first_child)
|
83
|
+
is_static = is_explicit || first_child.nil? || type?(:const).call(first_child)
|
84
|
+
[is_explicit, is_static]
|
85
|
+
end
|
86
|
+
|
87
|
+
def type?(type)
|
88
|
+
->(n) { n.respond_to?(:type) && n.type == type }
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Mudguard
|
4
|
+
module Domain
|
5
|
+
# Builds texts to be displayed to a user
|
6
|
+
module Texts
|
7
|
+
def summary(file_count, violation_count)
|
8
|
+
files = pluralize("file", file_count)
|
9
|
+
"#{file_count} #{files} inspected, #{count(violation_count)}\
|
10
|
+
bad #{dependency(violation_count)} detected"
|
11
|
+
end
|
12
|
+
|
13
|
+
def dependency_not_allowed(dependency)
|
14
|
+
"#{dependency} not allowed"
|
15
|
+
end
|
16
|
+
|
17
|
+
def dependency_summary(file_count, dependency_count)
|
18
|
+
files = pluralize("file", file_count)
|
19
|
+
"#{file_count} #{files} inspected, #{count(dependency_count)}\
|
20
|
+
good #{dependency(dependency_count)} detected"
|
21
|
+
end
|
22
|
+
|
23
|
+
def dependency_allowed(dependency)
|
24
|
+
dependency.to_s
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def dependency(count)
|
30
|
+
count <= 1 ? "dependency" : "dependencies"
|
31
|
+
end
|
32
|
+
|
33
|
+
def count(count)
|
34
|
+
count.zero? ? "no" : count.to_s
|
35
|
+
end
|
36
|
+
|
37
|
+
def pluralize(word, count)
|
38
|
+
count == 1 ? word : "#{word}s"
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "optparse"
|
4
|
+
require "mudguard/application/application"
|
5
|
+
require_relative "notification_adapter"
|
6
|
+
|
7
|
+
module Mudguard
|
8
|
+
module Infrastructure
|
9
|
+
module Cli
|
10
|
+
# Parses the cli arguments
|
11
|
+
class Controller
|
12
|
+
def initialize(view:) # rubocop:disable Metrics/MethodLength
|
13
|
+
@cmd = :analyse
|
14
|
+
@view = view
|
15
|
+
@display_opts = {
|
16
|
+
view: view,
|
17
|
+
compressed: false
|
18
|
+
}
|
19
|
+
@parser = ::OptionParser.new do |opts|
|
20
|
+
opts.banner = "Usage: mudguard [options] [directory]"
|
21
|
+
opts.on("-h", "--help", "Prints this help") do
|
22
|
+
@cmd = :help
|
23
|
+
end
|
24
|
+
opts.on("-p", "--print", "Prints all allowed dependencies") do
|
25
|
+
@cmd = :print_allowed
|
26
|
+
end
|
27
|
+
opts.on("-c", "--compressed", "Omits printing the same dependency more than once") do
|
28
|
+
@display_opts[:compressed] = true
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def parse!(argv) # rubocop:disable Metrics/MethodLength
|
34
|
+
directories = @parser.parse!(argv)
|
35
|
+
|
36
|
+
case @cmd
|
37
|
+
when :print_allowed
|
38
|
+
print_allowed_dependencies(directories)
|
39
|
+
when :help
|
40
|
+
help
|
41
|
+
when :analyse
|
42
|
+
check_dependencies(directories)
|
43
|
+
else
|
44
|
+
raise StandardError, "unknown command #{@cmd}"
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
def help
|
51
|
+
@view.print(@parser.to_s)
|
52
|
+
true
|
53
|
+
end
|
54
|
+
|
55
|
+
def check_dependencies(directories)
|
56
|
+
yield_directories(directories) do |directory, notification|
|
57
|
+
Mudguard::Application.check(directory, notification)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def print_allowed_dependencies(directories)
|
62
|
+
yield_directories(directories) do |directory, notification|
|
63
|
+
Mudguard::Application.print_allowed_dependencies(directory, notification)
|
64
|
+
true
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def yield_directories(directories)
|
69
|
+
notification = NotificationAdapter.new(**@display_opts)
|
70
|
+
directories = [Dir.pwd] if directories.empty?
|
71
|
+
directories.all? do |directory|
|
72
|
+
yield directory, notification
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Mudguard
|
4
|
+
module Infrastructure
|
5
|
+
module Cli
|
6
|
+
# Forwards Notification to the view for printing
|
7
|
+
class NotificationAdapter
|
8
|
+
def initialize(view:, compressed: false)
|
9
|
+
@view = view
|
10
|
+
@compressed = compressed
|
11
|
+
@printed_texts = Set.new
|
12
|
+
end
|
13
|
+
|
14
|
+
def add(location, text)
|
15
|
+
text = if location.nil? || @compressed
|
16
|
+
text
|
17
|
+
else
|
18
|
+
"#{location} #{text}"
|
19
|
+
end
|
20
|
+
@view.print(text) unless @printed_texts.include?(text)
|
21
|
+
@printed_texts << text
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|