mudguard 0.1.4 → 0.2.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 +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
|