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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 52da6086ca5ad7d9cc7882c0be051e35d1a1672930d616f3929d44909b23717f
4
- data.tar.gz: 84ba1b85b2d6cdec19ccbd80ffe6f39745054f3e8617a250c7dc613cd40dc00e
3
+ metadata.gz: 6db3d7377ad8b0b1d13e59c306bd937bcb39a2b68bee5d0539a4dff17d460243
4
+ data.tar.gz: b46919284fea9ad09e6db895025c50eeff3577fc0fe32bbc3b5ad7aec0e4833f
5
5
  SHA512:
6
- metadata.gz: d41681429a450ad75420b5cbe6bf78a261bc2d6d90be6547e080557d09b66c7a937c6798cd2f4603e20c7154d1540638f3861830ea4add5c5bce513d78a6dfbb
7
- data.tar.gz: e4f6a3925f3a65496c646cf2c1d0f5143b7e9f0c103a327961479b3dd01e4307bc743934ef8152ac73d15661bd04a842906876d595414814efdc6be285ba5efb
6
+ metadata.gz: 68ed243c036a7bdbe9a5e3e5e5996c76006cbc7057f4f5aad78d43c011038a6de16496c61ce5bd9c88a63c678f6d501f53af17e18e931abb20d0bce351ce0585
7
+ data.tar.gz: 050fdc731cc7297f986f10ed1b39e95aa6e82bf2f36959d7fe6e322c9681b5322a8a0390b177ced26ecc5c09bea737e86b26645966e4d1b2612eee25d370a735
data/.gitignore CHANGED
@@ -16,3 +16,7 @@
16
16
 
17
17
  # Ignore pkg (gem dir)
18
18
  /pkg
19
+ /tmp
20
+
21
+ # Following file is dynamically created while running tests
22
+ /spec/lib/infrastructure/persistence/policy_file_project/.mudguard.yml
@@ -1,7 +1,9 @@
1
1
  AllCops:
2
+ NewCops: enable
2
3
  Exclude:
3
4
  - 'bin/*'
4
5
  - 'spec/test_projects/**/*.rb'
6
+ - 'spec/lib/domain/source_examples/*.rb'
5
7
 
6
8
  Style/StringLiterals:
7
9
  EnforcedStyle: double_quotes
@@ -1,19 +1,20 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- mudguard (0.1.7)
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.0.1)
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.1)
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.4)
35
- method_source (0.9.2)
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.0.4)
41
+ parser (2.7.1.3)
42
42
  ast (~> 2.4.0)
43
- pry (0.12.2)
44
- coderay (~> 1.1.0)
45
- method_source (~> 0.9.0)
46
- pry-byebug (3.7.0)
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.10)
49
- pry-doc (1.0.0)
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.3)
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.1)
63
- rspec-support (~> 3.9.1)
64
- rspec-expectations (3.9.0)
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.2)
71
- rubocop (0.80.1)
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, < 1.7)
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.6.1)
83
- yard (0.9.24)
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/policy_file"
4
- require_relative "../infrastructure/persistence/ruby_files"
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
- policy_file_path = File.expand_path("MudguardFile", project_path)
13
- policies = Infrastructure::Persistence::PolicyFile.read(policy_file_path)
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
- policies.check(Infrastructure::Persistence::RubyFiles.all(project_path), notification)
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
- def initialize(policies: [])
8
- @policies = policies.map { |l| l.gsub(/\s/, "") }.map { |p| /^#{p}/ }
11
+ include Texts
12
+
13
+ def initialize(source_policies: [])
14
+ @source_policies = source_policies
9
15
  end
10
16
 
11
- def check(sources, notification)
12
- result = check_sources(sources, notification)
17
+ def check(notification)
18
+ result = analyse(:check, notification)
13
19
 
14
- count = result[:count]
15
- violations = result[:violations]
20
+ count = result[:sources_count]
21
+ violations = result[:analyser_count]
16
22
 
17
- files = pluralize("file", count)
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
- private
27
+ def print_allowed_dependencies(notification)
28
+ result = analyse(:print_allowed, notification)
24
29
 
25
- def pluralize(word, count)
26
- count == 1 ? word : "#{word}s"
27
- end
30
+ count = result[:sources_count]
31
+ violations = result[:analyser_count]
28
32
 
29
- def check_sources(sources, notification)
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
- def check_dependencies(dependencies, notification)
38
- dependencies.reject { |d| check_dependency(d, notification) }.count
39
- end
36
+ private
40
37
 
41
- def check_dependency(dependency, notification)
42
- is_allowed = @policies.any? { |p| dependency.match(p) }
43
- notification.add("#{dependency} not allowed") unless is_allowed
44
- is_allowed
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
- require "mudguard/domain/dependency"
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: nil, code:)
11
- @code = code
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
- @code == other.instance_eval { @code } && @location == other.instance_eval { @location }
20
+ @location == other.instance_eval { @location }
17
21
  end
18
22
 
19
- def inspect
20
- @location
23
+ def hash
24
+ @location.hash
21
25
  end
22
26
 
23
- def find_mod_dependencies
24
- begin
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
- private
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 create_dependency(module_name, node)
50
- const_name = find_const_name(node.children)
51
- return [] unless const_name&.include?("::")
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 ignore_and_continue(node, module_name)
63
- case node
64
- when children?
65
- node.children.flat_map { |c| process(c, module_name) }
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 process_module(children, module_name)
72
- const_name = find_const_name(children[0].children)
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
- def process_class(children, module_name)
82
- process(children[1], module_name)
83
- end
51
+ private
84
52
 
85
- def find_const_name(children)
86
- return nil if children.nil?
53
+ SYNTAX_ERROR = "error"
87
54
 
88
- module_name = find_const_name(children[0]&.children)
89
- const_name = children[1].to_s
90
- if module_name.nil?
91
- const_name
92
- else
93
- "#{module_name}::#{const_name}"
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 children?
98
- ->(n) { n.respond_to?(:children) }
68
+ def code
69
+ @code ||= @code_loader.call
99
70
  end
100
71
 
101
- def type?(type)
102
- ->(n) { n.respond_to?(:type) && n.type == type }
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,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mudguard
4
+ module Domain
5
+ # Associates a source with it's policies
6
+ SourcePolicies = Struct.new(:source, :policies, keyword_init: true)
7
+ end
8
+ 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
- @done = false
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
- view.print(opts.to_s)
19
- @done = true
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
- return true if @done
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
- notification = NotificationAdapter.new(view: @view)
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? { |d| Mudguard::Application.check(d, notification) }
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
- @view.print(text)
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
- def self.read(policy_file)
12
- policy_exists = File.exist?(policy_file)
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
- unless policy_exists
15
- raise Mudguard::Domain::Error, "expected policy file #{policy_file} doesn't exists"
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
- Mudguard::Domain::Policies.new(policies: File.readlines(policy_file))
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 all(project_path)
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
- enumerate_files(project_path)
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, "**", "*.rb")
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), code: File.read(f))
31
- end.lazy
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Mudguard
4
- VERSION = "0.1.7"
4
+ VERSION = "0.1.8"
5
5
  end
@@ -40,4 +40,5 @@ Gem::Specification.new do |spec|
40
40
  spec.add_development_dependency "rubocop", "~>0.80"
41
41
 
42
42
  spec.add_dependency "parser", "~>2.7"
43
+ spec.add_dependency "rake", "~> 13.0"
43
44
  end
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.7
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-03-28 00:00:00.000000000 Z
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