mudguard 0.1.7 → 0.1.8

Sign up to get free protection for your applications and to get access to all the features.
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