mudguard 0.1.7 → 0.1.8
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 +4 -0
- data/.rubocop.yml +2 -0
- data/Gemfile.lock +25 -23
- data/lib/mudguard/application/application.rb +19 -5
- 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 +2 -4
- data/lib/mudguard/domain/dependency_visitor.rb +32 -0
- data/lib/mudguard/domain/policies.rb +27 -26
- data/lib/mudguard/domain/source.rb +42 -69
- 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 +50 -8
- data/lib/mudguard/infrastructure/cli/notification_adapter.rb +11 -3
- data/lib/mudguard/infrastructure/persistence/policy_file.rb +27 -5
- data/lib/mudguard/infrastructure/persistence/project_repository.rb +31 -0
- data/lib/mudguard/infrastructure/persistence/ruby_files.rb +8 -6
- data/lib/mudguard/infrastructure/rake/task.rb +28 -0
- data/lib/mudguard/version.rb +1 -1
- data/mudguard.gemspec +1 -0
- metadata +25 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6db3d7377ad8b0b1d13e59c306bd937bcb39a2b68bee5d0539a4dff17d460243
|
4
|
+
data.tar.gz: b46919284fea9ad09e6db895025c50eeff3577fc0fe32bbc3b5ad7aec0e4833f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 68ed243c036a7bdbe9a5e3e5e5996c76006cbc7057f4f5aad78d43c011038a6de16496c61ce5bd9c88a63c678f6d501f53af17e18e931abb20d0bce351ce0585
|
7
|
+
data.tar.gz: 050fdc731cc7297f986f10ed1b39e95aa6e82bf2f36959d7fe6e322c9681b5322a8a0390b177ced26ecc5c09bea737e86b26645966e4d1b2612eee25d370a735
|
data/.gitignore
CHANGED
data/.rubocop.yml
CHANGED
data/Gemfile.lock
CHANGED
@@ -1,19 +1,20 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
mudguard (0.1.
|
4
|
+
mudguard (0.1.8)
|
5
5
|
parser (~> 2.7)
|
6
|
+
rake (~> 13.0)
|
6
7
|
|
7
8
|
GEM
|
8
9
|
remote: https://rubygems.org/
|
9
10
|
specs:
|
10
11
|
ast (2.4.0)
|
11
|
-
byebug (11.
|
12
|
+
byebug (11.1.3)
|
12
13
|
coderay (1.1.2)
|
13
14
|
diff-lcs (1.3)
|
14
15
|
ffi (1.12.2)
|
15
16
|
formatador (0.2.5)
|
16
|
-
guard (2.16.
|
17
|
+
guard (2.16.2)
|
17
18
|
formatador (>= 0.2.4)
|
18
19
|
listen (>= 2.7, < 4.0)
|
19
20
|
lumberjack (>= 1.0.12, < 2.0)
|
@@ -27,31 +28,30 @@ GEM
|
|
27
28
|
guard (~> 2.1)
|
28
29
|
guard-compat (~> 1.1)
|
29
30
|
rspec (>= 2.99.0, < 4.0)
|
30
|
-
jaro_winkler (1.5.4)
|
31
31
|
listen (3.2.1)
|
32
32
|
rb-fsevent (~> 0.10, >= 0.10.3)
|
33
33
|
rb-inotify (~> 0.9, >= 0.9.10)
|
34
|
-
lumberjack (1.2.
|
35
|
-
method_source (0.
|
34
|
+
lumberjack (1.2.5)
|
35
|
+
method_source (1.0.0)
|
36
36
|
nenv (0.3.0)
|
37
37
|
notiffany (0.1.3)
|
38
38
|
nenv (~> 0.1)
|
39
39
|
shellany (~> 0.0)
|
40
40
|
parallel (1.19.1)
|
41
|
-
parser (2.7.
|
41
|
+
parser (2.7.1.3)
|
42
42
|
ast (~> 2.4.0)
|
43
|
-
pry (0.
|
44
|
-
coderay (~> 1.1
|
45
|
-
method_source (~>
|
46
|
-
pry-byebug (3.
|
43
|
+
pry (0.13.1)
|
44
|
+
coderay (~> 1.1)
|
45
|
+
method_source (~> 1.0)
|
46
|
+
pry-byebug (3.9.0)
|
47
47
|
byebug (~> 11.0)
|
48
|
-
pry (~> 0.
|
49
|
-
pry-doc (1.
|
48
|
+
pry (~> 0.13.0)
|
49
|
+
pry-doc (1.1.0)
|
50
50
|
pry (~> 0.11)
|
51
51
|
yard (~> 0.9.11)
|
52
52
|
rainbow (3.0.0)
|
53
53
|
rake (13.0.1)
|
54
|
-
rb-fsevent (0.10.
|
54
|
+
rb-fsevent (0.10.4)
|
55
55
|
rb-inotify (0.10.1)
|
56
56
|
ffi (~> 1.0)
|
57
57
|
rexml (3.2.4)
|
@@ -59,28 +59,30 @@ GEM
|
|
59
59
|
rspec-core (~> 3.9.0)
|
60
60
|
rspec-expectations (~> 3.9.0)
|
61
61
|
rspec-mocks (~> 3.9.0)
|
62
|
-
rspec-core (3.9.
|
63
|
-
rspec-support (~> 3.9.
|
64
|
-
rspec-expectations (3.9.
|
62
|
+
rspec-core (3.9.2)
|
63
|
+
rspec-support (~> 3.9.3)
|
64
|
+
rspec-expectations (3.9.2)
|
65
65
|
diff-lcs (>= 1.2.0, < 2.0)
|
66
66
|
rspec-support (~> 3.9.0)
|
67
67
|
rspec-mocks (3.9.1)
|
68
68
|
diff-lcs (>= 1.2.0, < 2.0)
|
69
69
|
rspec-support (~> 3.9.0)
|
70
|
-
rspec-support (3.9.
|
71
|
-
rubocop (0.
|
72
|
-
jaro_winkler (~> 1.5.1)
|
70
|
+
rspec-support (3.9.3)
|
71
|
+
rubocop (0.84.0)
|
73
72
|
parallel (~> 1.10)
|
74
73
|
parser (>= 2.7.0.1)
|
75
74
|
rainbow (>= 2.2.2, < 4.0)
|
76
75
|
rexml
|
76
|
+
rubocop-ast (>= 0.0.3)
|
77
77
|
ruby-progressbar (~> 1.7)
|
78
|
-
unicode-display_width (>= 1.4.0, <
|
78
|
+
unicode-display_width (>= 1.4.0, < 2.0)
|
79
|
+
rubocop-ast (0.0.3)
|
80
|
+
parser (>= 2.7.0.1)
|
79
81
|
ruby-progressbar (1.10.1)
|
80
82
|
shellany (0.0.1)
|
81
83
|
thor (1.0.1)
|
82
|
-
unicode-display_width (1.
|
83
|
-
yard (0.9.
|
84
|
+
unicode-display_width (1.7.0)
|
85
|
+
yard (0.9.25)
|
84
86
|
|
85
87
|
PLATFORMS
|
86
88
|
ruby
|
@@ -1,7 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require_relative "../infrastructure/persistence/
|
4
|
-
require_relative "../
|
3
|
+
require_relative "../infrastructure/persistence/project_repository"
|
4
|
+
require_relative "../domain/policies"
|
5
5
|
|
6
6
|
# Contains methods to check if your project is a bit muddy
|
7
7
|
module Mudguard
|
@@ -9,10 +9,24 @@ module Mudguard
|
|
9
9
|
module Application
|
10
10
|
class << self
|
11
11
|
def check(project_path, notification)
|
12
|
-
|
13
|
-
|
12
|
+
create_policies(project_path) do |policies|
|
13
|
+
policies.check(notification)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def print_allowed_dependencies(project_path, notification)
|
18
|
+
create_policies(project_path) do |policies|
|
19
|
+
policies.print_allowed_dependencies(notification)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
14
24
|
|
15
|
-
|
25
|
+
def create_policies(project_path)
|
26
|
+
repo = Infrastructure::Persistence::ProjectRepository
|
27
|
+
source_policies = repo.load_source_policies(project_path)
|
28
|
+
policies = Domain::Policies.new(source_policies: source_policies)
|
29
|
+
yield policies
|
16
30
|
end
|
17
31
|
end
|
18
32
|
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Mudguard
|
4
|
+
module Domain
|
5
|
+
# Transforms AST-Nodes into Strings denoting consts
|
6
|
+
class ConstVisitor
|
7
|
+
def initialize
|
8
|
+
@consts = []
|
9
|
+
end
|
10
|
+
|
11
|
+
attr_reader :consts
|
12
|
+
|
13
|
+
def visit_dependency(_, __, ___); end # rubocop:disable Naming/MethodParameterName
|
14
|
+
|
15
|
+
def visit_const_declaration(_location, const_name, module_name)
|
16
|
+
@consts << "#{module_name}#{const_name}"
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "error"
|
4
|
+
|
5
|
+
module Mudguard
|
6
|
+
module Domain
|
7
|
+
# Knows all constants of the project
|
8
|
+
class Consts
|
9
|
+
def initialize(sources:)
|
10
|
+
@consts = sources
|
11
|
+
.flat_map(&:find_consts)
|
12
|
+
.each_with_object(Hash.new { |h, k| h[k] = {} }) do |c, a|
|
13
|
+
path = split_hierarchy(c)
|
14
|
+
const_name = path.last
|
15
|
+
module_names = path.take(path.count - 1)
|
16
|
+
sub_module = module_names.reduce(a) { |h, m| h[m] }
|
17
|
+
sub_module[const_name] = {} unless sub_module.key?(const_name)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def resolve(module_name, const_name)
|
22
|
+
raise Error, "const_name is undefined" if const_name.empty?
|
23
|
+
|
24
|
+
path = split_hierarchy(module_name)
|
25
|
+
if module_name.empty?
|
26
|
+
# not in a module therefor const can only be defined in the root module (::)
|
27
|
+
qualified_path(const_name)
|
28
|
+
else
|
29
|
+
# analyse module hierarchy to find fully qualified const name
|
30
|
+
# resolve_in_modules(const_name, path)
|
31
|
+
const_path = const_name.split(SEPARATOR).drop(1)
|
32
|
+
find_const_deeper("", path, @consts, const_path) || qualified_path(const_name)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
SEPARATOR = "::"
|
39
|
+
|
40
|
+
def find_const_deeper(current_module, remaining_modules, consts, const_path)
|
41
|
+
return if consts.nil?
|
42
|
+
|
43
|
+
if remaining_modules.any?
|
44
|
+
# Move deeper toward target module
|
45
|
+
next_module = remaining_modules.first
|
46
|
+
next_remaining = remaining_modules.drop(1)
|
47
|
+
next_consts = consts[next_module]
|
48
|
+
found_const = find_const_deeper(next_module, next_remaining, next_consts, const_path)
|
49
|
+
return join_path(current_module, found_const) if found_const
|
50
|
+
end
|
51
|
+
|
52
|
+
find_const(current_module, consts, const_path)
|
53
|
+
end
|
54
|
+
|
55
|
+
def find_const(current_module, consts, const_path)
|
56
|
+
const_name = const_path.first
|
57
|
+
if const_path.length == 1 && consts.key?(const_name)
|
58
|
+
# const defined in current_module
|
59
|
+
return join_path(current_module, const_name)
|
60
|
+
end
|
61
|
+
|
62
|
+
# backward search (along const_path only)
|
63
|
+
next_path = const_path.drop(1)
|
64
|
+
found_const = find_const_deeper(const_name, next_path, consts[const_name], next_path)
|
65
|
+
found_const ? join_path(current_module, found_const) : nil
|
66
|
+
end
|
67
|
+
|
68
|
+
def join_path(module_name, const_name)
|
69
|
+
"#{module_name}#{SEPARATOR}#{const_name}"
|
70
|
+
end
|
71
|
+
|
72
|
+
def qualified_path(const_name)
|
73
|
+
const_name =~ /^#{SEPARATOR}/ ? const_name : "#{SEPARATOR}#{const_name}"
|
74
|
+
end
|
75
|
+
|
76
|
+
def split_hierarchy(module_name)
|
77
|
+
module_name.split(SEPARATOR).drop(1)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "texts"
|
4
|
+
|
5
|
+
module Mudguard
|
6
|
+
module Domain
|
7
|
+
# Executes operation on a set of dependencies
|
8
|
+
class Dependencies
|
9
|
+
include Texts
|
10
|
+
|
11
|
+
def initialize(policies:, notification:)
|
12
|
+
@policies = policies.map { |p| /^#{p}/x }
|
13
|
+
@notification = notification
|
14
|
+
end
|
15
|
+
|
16
|
+
def check(dependencies)
|
17
|
+
select_dependencies(dependencies) do |dependency, is_allowed|
|
18
|
+
add_message(dependency, dependency_not_allowed(dependency.dependency)) unless is_allowed
|
19
|
+
!is_allowed
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def print_allowed(dependencies)
|
24
|
+
select_dependencies(dependencies) do |dependency, is_allowed|
|
25
|
+
add_message(dependency, dependency_allowed(dependency.dependency)) if is_allowed
|
26
|
+
is_allowed
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def add_message(dependency, message)
|
33
|
+
@notification.add(dependency.location, message)
|
34
|
+
end
|
35
|
+
|
36
|
+
def select_dependencies(dependencies)
|
37
|
+
dependencies.select do |dependency|
|
38
|
+
yield dependency, dependency_allowed?(dependency)
|
39
|
+
end.count
|
40
|
+
end
|
41
|
+
|
42
|
+
def dependency_allowed?(dependency)
|
43
|
+
@policies.any? { |p| dependency.match(p) }
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -4,6 +4,8 @@ module Mudguard
|
|
4
4
|
module Domain
|
5
5
|
# A Dependency between Modules
|
6
6
|
class Dependency
|
7
|
+
attr_reader :location, :dependency
|
8
|
+
|
7
9
|
def initialize(location: nil, dependency:)
|
8
10
|
@location = location
|
9
11
|
@dependency = dependency
|
@@ -13,10 +15,6 @@ module Mudguard
|
|
13
15
|
"{#{@location}, #{@dependency}}"
|
14
16
|
end
|
15
17
|
|
16
|
-
def to_s
|
17
|
-
"#{@location} #{@dependency}"
|
18
|
-
end
|
19
|
-
|
20
18
|
def match(policy)
|
21
19
|
@dependency.match(policy)
|
22
20
|
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
|
@@ -1,47 +1,48 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require_relative "dependencies"
|
4
|
+
require_relative "texts"
|
5
|
+
require_relative "consts"
|
6
|
+
|
3
7
|
module Mudguard
|
4
8
|
module Domain
|
5
9
|
# Contains the policies to be enforced
|
6
10
|
class Policies
|
7
|
-
|
8
|
-
|
11
|
+
include Texts
|
12
|
+
|
13
|
+
def initialize(source_policies: [])
|
14
|
+
@source_policies = source_policies
|
9
15
|
end
|
10
16
|
|
11
|
-
def check(
|
12
|
-
result =
|
17
|
+
def check(notification)
|
18
|
+
result = analyse(:check, notification)
|
13
19
|
|
14
|
-
count = result[:
|
15
|
-
violations = result[:
|
20
|
+
count = result[:sources_count]
|
21
|
+
violations = result[:analyser_count]
|
16
22
|
|
17
|
-
|
18
|
-
problems = pluralize("problem", violations)
|
19
|
-
notification.add("#{count} #{files} inspected, #{violations} #{problems} detected")
|
23
|
+
notification.add(nil, summary(count, violations))
|
20
24
|
violations.zero?
|
21
25
|
end
|
22
26
|
|
23
|
-
|
27
|
+
def print_allowed_dependencies(notification)
|
28
|
+
result = analyse(:print_allowed, notification)
|
24
29
|
|
25
|
-
|
26
|
-
|
27
|
-
end
|
30
|
+
count = result[:sources_count]
|
31
|
+
violations = result[:analyser_count]
|
28
32
|
|
29
|
-
|
30
|
-
sources.each_with_object(count: 0, violations: 0) do |source, result|
|
31
|
-
result[:count] += 1
|
32
|
-
dependencies = source.find_mod_dependencies
|
33
|
-
result[:violations] += check_dependencies(dependencies, notification)
|
34
|
-
end
|
33
|
+
notification.add(nil, dependency_summary(count, violations))
|
35
34
|
end
|
36
35
|
|
37
|
-
|
38
|
-
dependencies.reject { |d| check_dependency(d, notification) }.count
|
39
|
-
end
|
36
|
+
private
|
40
37
|
|
41
|
-
def
|
42
|
-
|
43
|
-
|
44
|
-
|
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
|
45
46
|
end
|
46
47
|
end
|
47
48
|
end
|
@@ -1,105 +1,78 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require "parser/current"
|
4
|
-
|
4
|
+
require_relative "dependency"
|
5
|
+
require_relative "dependency_visitor"
|
6
|
+
require_relative "const_visitor"
|
7
|
+
require_relative "error"
|
8
|
+
require_relative "source_processor"
|
5
9
|
|
6
10
|
module Mudguard
|
7
11
|
module Domain
|
8
12
|
# Represents a Ruby source file
|
9
13
|
class Source
|
10
|
-
def initialize(location:
|
11
|
-
@
|
14
|
+
def initialize(location:, code_loader: -> { "" })
|
15
|
+
@code_loader = code_loader
|
12
16
|
@location = location
|
13
17
|
end
|
14
18
|
|
15
19
|
def ==(other)
|
16
|
-
@
|
20
|
+
@location == other.instance_eval { @location }
|
17
21
|
end
|
18
22
|
|
19
|
-
def
|
20
|
-
@location
|
23
|
+
def hash
|
24
|
+
@location.hash
|
21
25
|
end
|
22
26
|
|
23
|
-
def
|
24
|
-
|
25
|
-
root = Parser::CurrentRuby.parse(@code)
|
26
|
-
rescue Parser::SyntaxError
|
27
|
-
return []
|
28
|
-
end
|
29
|
-
return [] if root.nil?
|
30
|
-
|
31
|
-
process(root)
|
27
|
+
def eql?(other)
|
28
|
+
@location.eql?(other.instance_eval { @location })
|
32
29
|
end
|
33
30
|
|
34
|
-
|
35
|
-
|
36
|
-
def process(node, module_name = "")
|
37
|
-
case node
|
38
|
-
when type?(:module)
|
39
|
-
process_module(node.children, module_name)
|
40
|
-
when type?(:class)
|
41
|
-
process_class(node.children, module_name)
|
42
|
-
when type?(:const)
|
43
|
-
create_dependency(module_name, node)
|
44
|
-
else
|
45
|
-
ignore_and_continue(node, module_name)
|
46
|
-
end
|
31
|
+
def inspect
|
32
|
+
@location
|
47
33
|
end
|
48
34
|
|
49
|
-
def
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
dependency = if module_name.empty?
|
54
|
-
const_name
|
55
|
-
else
|
56
|
-
"#{module_name}->#{const_name}"
|
57
|
-
end
|
58
|
-
location = "#{@location}:#{node.location.line}"
|
59
|
-
[Dependency.new(location: location, dependency: dependency)]
|
35
|
+
def find_mod_dependencies(consts)
|
36
|
+
visitor = DependencyVisitor.new(consts: consts)
|
37
|
+
visit_ast(visitor)
|
38
|
+
visitor.dependencies
|
60
39
|
end
|
61
40
|
|
62
|
-
def
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
else
|
67
|
-
[]
|
68
|
-
end
|
41
|
+
def find_consts
|
42
|
+
visitor = ConstVisitor.new
|
43
|
+
visit_ast(visitor)
|
44
|
+
visitor.consts
|
69
45
|
end
|
70
46
|
|
71
|
-
def
|
72
|
-
|
73
|
-
module_name = if module_name.empty?
|
74
|
-
const_name
|
75
|
-
else
|
76
|
-
"#{module_name}::#{const_name}"
|
77
|
-
end
|
78
|
-
process(children[1], module_name)
|
47
|
+
def location?(glob)
|
48
|
+
@location == glob
|
79
49
|
end
|
80
50
|
|
81
|
-
|
82
|
-
process(children[1], module_name)
|
83
|
-
end
|
51
|
+
private
|
84
52
|
|
85
|
-
|
86
|
-
return nil if children.nil?
|
53
|
+
SYNTAX_ERROR = "error"
|
87
54
|
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
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
|
94
64
|
end
|
65
|
+
root.nil? ? SYNTAX_ERROR : root
|
95
66
|
end
|
96
67
|
|
97
|
-
def
|
98
|
-
|
68
|
+
def code
|
69
|
+
@code ||= @code_loader.call
|
99
70
|
end
|
100
71
|
|
101
|
-
def
|
102
|
-
|
72
|
+
def visit_ast(visitor)
|
73
|
+
return if ast == SYNTAX_ERROR
|
74
|
+
|
75
|
+
SourceProcessor.new(location: @location).process(ast, visitor)
|
103
76
|
end
|
104
77
|
end
|
105
78
|
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
|
@@ -9,26 +9,68 @@ module Mudguard
|
|
9
9
|
module Cli
|
10
10
|
# Parses the cli arguments
|
11
11
|
class Controller
|
12
|
-
def initialize(view:)
|
12
|
+
def initialize(view:) # rubocop:disable Metrics/MethodLength
|
13
|
+
@cmd = :analyse
|
13
14
|
@view = view
|
14
|
-
@
|
15
|
+
@display_opts = {
|
16
|
+
view: view,
|
17
|
+
compressed: false
|
18
|
+
}
|
15
19
|
@parser = ::OptionParser.new do |opts|
|
16
20
|
opts.banner = "Usage: mudguard [options] [directory]"
|
17
21
|
opts.on("-h", "--help", "Prints this help") do
|
18
|
-
|
19
|
-
|
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
|
20
29
|
end
|
21
30
|
end
|
22
31
|
end
|
23
32
|
|
24
|
-
def parse!(argv)
|
33
|
+
def parse!(argv) # rubocop:disable Metrics/MethodLength
|
25
34
|
directories = @parser.parse!(argv)
|
26
35
|
|
27
|
-
|
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
|
28
60
|
|
29
|
-
|
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)
|
30
70
|
directories = [Dir.pwd] if directories.empty?
|
31
|
-
directories.all?
|
71
|
+
directories.all? do |directory|
|
72
|
+
yield directory, notification
|
73
|
+
end
|
32
74
|
end
|
33
75
|
end
|
34
76
|
end
|
@@ -5,12 +5,20 @@ module Mudguard
|
|
5
5
|
module Cli
|
6
6
|
# Forwards Notification to the view for printing
|
7
7
|
class NotificationAdapter
|
8
|
-
def initialize(view:)
|
8
|
+
def initialize(view:, compressed: false)
|
9
9
|
@view = view
|
10
|
+
@compressed = compressed
|
11
|
+
@printed_texts = Set.new
|
10
12
|
end
|
11
13
|
|
12
|
-
def add(text)
|
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
|
14
22
|
end
|
15
23
|
end
|
16
24
|
end
|
@@ -1,5 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require "yaml"
|
3
4
|
require_relative "../../domain/policies"
|
4
5
|
require_relative "../../domain/error"
|
5
6
|
|
@@ -8,14 +9,35 @@ module Mudguard
|
|
8
9
|
module Persistence
|
9
10
|
# A file containing the Mudguard-Policies
|
10
11
|
class PolicyFile
|
11
|
-
|
12
|
-
|
12
|
+
class << self
|
13
|
+
def read(project_path)
|
14
|
+
policy_file = File.join(project_path, ".mudguard.yml")
|
15
|
+
policy_exists = File.exist?(policy_file)
|
13
16
|
|
14
|
-
|
15
|
-
|
17
|
+
unless policy_exists
|
18
|
+
raise Mudguard::Domain::Error, "expected policy file #{policy_file} doesn't exists"
|
19
|
+
end
|
20
|
+
|
21
|
+
read_yml(policy_file)
|
16
22
|
end
|
17
23
|
|
18
|
-
|
24
|
+
private
|
25
|
+
|
26
|
+
def read_yml(policy_file)
|
27
|
+
yaml_file = File.read(policy_file)
|
28
|
+
yaml = YAML.safe_load(yaml_file, [Symbol], [], policy_file) || {}
|
29
|
+
yaml.transform_values { |value| (value || []).map(&method(:unsymbolize)) }
|
30
|
+
rescue Psych::SyntaxError => e
|
31
|
+
raise Mudguard::Domain::Error, "#{policy_file} is invalid (#{e.message})"
|
32
|
+
end
|
33
|
+
|
34
|
+
def unsymbolize(dependency)
|
35
|
+
if dependency.is_a?(Symbol)
|
36
|
+
":#{dependency}"
|
37
|
+
else
|
38
|
+
dependency
|
39
|
+
end
|
40
|
+
end
|
19
41
|
end
|
20
42
|
end
|
21
43
|
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "policy_file"
|
4
|
+
require_relative "ruby_files"
|
5
|
+
require_relative "../../domain/source_policies"
|
6
|
+
|
7
|
+
module Mudguard
|
8
|
+
module Infrastructure
|
9
|
+
module Persistence
|
10
|
+
# Provides access to the persisted source and policies
|
11
|
+
class ProjectRepository
|
12
|
+
class << self
|
13
|
+
def load_source_policies(project_path)
|
14
|
+
file = PolicyFile.read(project_path)
|
15
|
+
scopes = file.flat_map do |patterns, policies|
|
16
|
+
sources = RubyFiles.select(project_path, patterns: [patterns])
|
17
|
+
sources.flat_map { |s| { source: s, policies: policies } }
|
18
|
+
end
|
19
|
+
|
20
|
+
sources = scopes.group_by { |e| e[:source] }
|
21
|
+
|
22
|
+
sources.map do |source, group|
|
23
|
+
policies = group.flat_map { |r| r[:policies] }.uniq
|
24
|
+
Domain::SourcePolicies.new(source: source, policies: policies)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -9,26 +9,28 @@ module Mudguard
|
|
9
9
|
# Provides access to all ruby-files of a project
|
10
10
|
class RubyFiles
|
11
11
|
class << self
|
12
|
-
def
|
12
|
+
def select(project_path, patterns: nil)
|
13
13
|
project_exists = Dir.exist?(project_path)
|
14
14
|
|
15
15
|
unless project_exists
|
16
16
|
raise Mudguard::Domain::Error, "expected project #{project_path} doesn't exists"
|
17
17
|
end
|
18
18
|
|
19
|
-
|
19
|
+
patterns = [File.join("**", "*.rb")] if patterns.nil?
|
20
|
+
enumerate_files(project_path, patterns)
|
20
21
|
end
|
21
22
|
|
22
23
|
private
|
23
24
|
|
24
|
-
def enumerate_files(project_path)
|
25
|
+
def enumerate_files(project_path, patterns)
|
25
26
|
project_path_name = Pathname.new(project_path)
|
26
|
-
ruby_files = File.join(project_path,
|
27
|
+
ruby_files = patterns.map { |p| File.join(project_path, p) }
|
27
28
|
Dir.glob(ruby_files).map do |f|
|
28
29
|
file_path_name = Pathname.new(f)
|
29
30
|
diff_path = file_path_name.relative_path_from(project_path_name).to_s
|
30
|
-
Mudguard::Domain::Source.new(location: File.join("./", diff_path),
|
31
|
-
|
31
|
+
Mudguard::Domain::Source.new(location: File.join("./", diff_path),
|
32
|
+
code_loader: -> { File.read(f) })
|
33
|
+
end
|
32
34
|
end
|
33
35
|
end
|
34
36
|
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "rake"
|
4
|
+
require "rake/tasklib"
|
5
|
+
require_relative "../../application/application"
|
6
|
+
require_relative "../../infrastructure/cli/notification_adapter"
|
7
|
+
require_relative "../../infrastructure/cli/view"
|
8
|
+
|
9
|
+
module Mudguard
|
10
|
+
module Infrastructure
|
11
|
+
module Rake
|
12
|
+
# Provides Mudguard Rake Tasks
|
13
|
+
class Task < ::Rake::TaskLib
|
14
|
+
def initialize(project_dir: Dir.pwd)
|
15
|
+
@project_dir = project_dir
|
16
|
+
|
17
|
+
desc "Run Mudguard"
|
18
|
+
task(:mudguard) do
|
19
|
+
view = Mudguard::Infrastructure::Cli::View.new
|
20
|
+
notification = Mudguard::Infrastructure::Cli::NotificationAdapter.new(view: view)
|
21
|
+
ok = Application.check(@project_dir, notification)
|
22
|
+
abort unless ok
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
data/lib/mudguard/version.rb
CHANGED
data/mudguard.gemspec
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: mudguard
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.8
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jorg Jenni
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2020-
|
11
|
+
date: 2020-06-07 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -150,6 +150,20 @@ dependencies:
|
|
150
150
|
- - "~>"
|
151
151
|
- !ruby/object:Gem::Version
|
152
152
|
version: '2.7'
|
153
|
+
- !ruby/object:Gem::Dependency
|
154
|
+
name: rake
|
155
|
+
requirement: !ruby/object:Gem::Requirement
|
156
|
+
requirements:
|
157
|
+
- - "~>"
|
158
|
+
- !ruby/object:Gem::Version
|
159
|
+
version: '13.0'
|
160
|
+
type: :runtime
|
161
|
+
prerelease: false
|
162
|
+
version_requirements: !ruby/object:Gem::Requirement
|
163
|
+
requirements:
|
164
|
+
- - "~>"
|
165
|
+
- !ruby/object:Gem::Version
|
166
|
+
version: '13.0'
|
153
167
|
description:
|
154
168
|
email:
|
155
169
|
- jorg.jenni@jennius.co.uk
|
@@ -172,15 +186,24 @@ files:
|
|
172
186
|
- exe/mudguard
|
173
187
|
- lib/mudguard.rb
|
174
188
|
- lib/mudguard/application/application.rb
|
189
|
+
- lib/mudguard/domain/const_visitor.rb
|
190
|
+
- lib/mudguard/domain/consts.rb
|
191
|
+
- lib/mudguard/domain/dependencies.rb
|
175
192
|
- lib/mudguard/domain/dependency.rb
|
193
|
+
- lib/mudguard/domain/dependency_visitor.rb
|
176
194
|
- lib/mudguard/domain/error.rb
|
177
195
|
- lib/mudguard/domain/policies.rb
|
178
196
|
- lib/mudguard/domain/source.rb
|
197
|
+
- lib/mudguard/domain/source_policies.rb
|
198
|
+
- lib/mudguard/domain/source_processor.rb
|
199
|
+
- lib/mudguard/domain/texts.rb
|
179
200
|
- lib/mudguard/infrastructure/cli/controller.rb
|
180
201
|
- lib/mudguard/infrastructure/cli/notification_adapter.rb
|
181
202
|
- lib/mudguard/infrastructure/cli/view.rb
|
182
203
|
- lib/mudguard/infrastructure/persistence/policy_file.rb
|
204
|
+
- lib/mudguard/infrastructure/persistence/project_repository.rb
|
183
205
|
- lib/mudguard/infrastructure/persistence/ruby_files.rb
|
206
|
+
- lib/mudguard/infrastructure/rake/task.rb
|
184
207
|
- lib/mudguard/version.rb
|
185
208
|
- lib/tasks/rubocop.rake
|
186
209
|
- lib/tasks/test.rake
|