mudguard 0.1.3 → 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: '076099989a94de468e2c43e3abe2ee8cfc1c1ab5a0f7b25291f609e597f1e04c'
4
- data.tar.gz: dff2e30cf95d50679ec2f03a65944dfc84c96f7daa2bf26557a790ba325d6a4d
3
+ metadata.gz: 6db3d7377ad8b0b1d13e59c306bd937bcb39a2b68bee5d0539a4dff17d460243
4
+ data.tar.gz: b46919284fea9ad09e6db895025c50eeff3577fc0fe32bbc3b5ad7aec0e4833f
5
5
  SHA512:
6
- metadata.gz: 66ca1a68eb9b7f12864da3c83ef74df51afb305cb1fb4ccbdccb267e9189ba2adfdc9c756b69637fab29ae8a28e5c5d246a1cdfc4d3beab1697f82d7fc3de704
7
- data.tar.gz: 54b2984f6c619c21e59c2056b1237dd05ff4d8915eb45570d69a119f4b676fd30046b215106c7142ebfc40ac4a4b5cbc681b7b7be7bc828c704c0a0022707185
6
+ metadata.gz: 68ed243c036a7bdbe9a5e3e5e5996c76006cbc7057f4f5aad78d43c011038a6de16496c61ce5bd9c88a63c678f6d501f53af17e18e931abb20d0bce351ce0585
7
+ data.tar.gz: 050fdc731cc7297f986f10ed1b39e95aa6e82bf2f36959d7fe6e322c9681b5322a8a0390b177ced26ecc5c09bea737e86b26645966e4d1b2612eee25d370a735
data/.gitignore CHANGED
@@ -13,3 +13,10 @@
13
13
  # Ignore RSpec Working Dir
14
14
  /.rspec
15
15
  .rspec_status
16
+
17
+ # Ignore pkg (gem dir)
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
- - 'spec/lib/test_projects/**/*.rb'
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.3)
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
data/Guardfile CHANGED
@@ -8,7 +8,9 @@ rspec_options = {
8
8
 
9
9
  guard "rspec", rspec_options do
10
10
  watch(%r{^spec/.+_spec\.rb$})
11
+ watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" }
11
12
  watch(%r{^lib/mudguard/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" }
12
13
  watch(%r{^lib/mudguard/(.+)\.rb$}) { |m| Dir.glob("spec/lib/#{m[1]}_*.rb") }
13
14
  watch("spec/spec_helper.rb") { "spec" }
15
+ watch(%r{^exe\/mudguard$}) { |_| "spec/exe/mudguard_spec.rb" }
14
16
  end
@@ -1,5 +1,13 @@
1
1
  #!/usr/bin/env ruby
2
- require "mudguard"
2
+ # frozen_string_literal: true
3
3
 
4
- Mudguard.check("")
5
- puts "OK"
4
+ require "mudguard/infrastructure/cli/view"
5
+ require "mudguard/infrastructure/cli/controller"
6
+
7
+ # Checks the dependencies of a ruby project
8
+ module Mudguard
9
+ view = Mudguard::Infrastructure::Cli::View.new
10
+ parser = Mudguard::Infrastructure::Cli::Controller.new(view: view)
11
+ ok = parser.parse!(ARGV)
12
+ exit(ok ? 0 : 1)
13
+ end
@@ -1,19 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "mudguard/version"
4
- require_relative "mudguard/policy_file"
5
- require_relative "mudguard/ruby_files"
6
-
7
- # Contains methods to check if your project is a bit muddy
8
- module Mudguard
9
- class << self
10
- def check(project_path)
11
- policy_file_path = File.expand_path("MudguardFile", project_path)
12
- policies = PolicyFile.read(policy_file_path)
13
-
14
- files = RubyFiles.all(project_path).map { |f| File.read(f) }
15
-
16
- policies.check(files)
17
- end
18
- end
19
- end
3
+ require_relative "./mudguard/version"
4
+ require_relative "./mudguard/application/application"
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../infrastructure/persistence/project_repository"
4
+ require_relative "../domain/policies"
5
+
6
+ # Contains methods to check if your project is a bit muddy
7
+ module Mudguard
8
+ # API to mudguard
9
+ module Application
10
+ class << self
11
+ def check(project_path, notification)
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
24
+
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
30
+ end
31
+ end
32
+ end
33
+ 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
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mudguard
4
+ module Domain
5
+ # A Dependency between Modules
6
+ class Dependency
7
+ attr_reader :location, :dependency
8
+
9
+ def initialize(location: nil, dependency:)
10
+ @location = location
11
+ @dependency = dependency
12
+ end
13
+
14
+ def inspect
15
+ "{#{@location}, #{@dependency}}"
16
+ end
17
+
18
+ def match(policy)
19
+ @dependency.match(policy)
20
+ end
21
+
22
+ def ==(other)
23
+ @location == other.instance_eval { @location } &&
24
+ @dependency == other.instance_eval { @dependency }
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mudguard
4
+ module Domain
5
+ # Transforms AST-Nodes into Dependencies
6
+ class DependencyVisitor
7
+ def initialize(consts:)
8
+ @consts = consts
9
+ @dependencies = []
10
+ end
11
+
12
+ attr_reader :dependencies
13
+
14
+ def visit_dependency(location, const_name, module_name)
15
+ qualified_const_name = @consts.resolve(module_name, const_name)
16
+ return [] unless qualified_const_name&.include?("::")
17
+
18
+ dependency = if module_name.empty?
19
+ "->#{qualified_const_name}"
20
+ else
21
+ "#{module_name}->#{qualified_const_name}"
22
+ end
23
+
24
+ @dependencies << Dependency.new(location: location, dependency: dependency)
25
+ end
26
+
27
+ # rubocop:disable Naming/MethodParameterName
28
+ def visit_const_declaration(_, __, ___); end
29
+ # rubocop:enable Naming/MethodParameterName
30
+ end
31
+ end
32
+ end
@@ -1,6 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Mudguard
4
- class Error < StandardError
4
+ module Domain
5
+ class Error < StandardError
6
+ end
5
7
  end
6
8
  end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "dependencies"
4
+ require_relative "texts"
5
+ require_relative "consts"
6
+
7
+ module Mudguard
8
+ module Domain
9
+ # Contains the policies to be enforced
10
+ class Policies
11
+ include Texts
12
+
13
+ def initialize(source_policies: [])
14
+ @source_policies = source_policies
15
+ end
16
+
17
+ def check(notification)
18
+ result = analyse(:check, notification)
19
+
20
+ count = result[:sources_count]
21
+ violations = result[:analyser_count]
22
+
23
+ notification.add(nil, summary(count, violations))
24
+ violations.zero?
25
+ end
26
+
27
+ def print_allowed_dependencies(notification)
28
+ result = analyse(:print_allowed, notification)
29
+
30
+ count = result[:sources_count]
31
+ violations = result[:analyser_count]
32
+
33
+ notification.add(nil, dependency_summary(count, violations))
34
+ end
35
+
36
+ private
37
+
38
+ def analyse(method, notification)
39
+ consts = Consts.new(sources: @source_policies.map(&:source))
40
+ @source_policies.each_with_object(sources_count: 0, analyser_count: 0) do |sp, result|
41
+ analyser = Dependencies.new(policies: sp.policies, notification: notification)
42
+ dependencies = sp.source.find_mod_dependencies(consts)
43
+ result[:sources_count] += 1
44
+ result[:analyser_count] += analyser.send(method, dependencies)
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "parser/current"
4
+ require_relative "dependency"
5
+ require_relative "dependency_visitor"
6
+ require_relative "const_visitor"
7
+ require_relative "error"
8
+ require_relative "source_processor"
9
+
10
+ module Mudguard
11
+ module Domain
12
+ # Represents a Ruby source file
13
+ class Source
14
+ def initialize(location:, code_loader: -> { "" })
15
+ @code_loader = code_loader
16
+ @location = location
17
+ end
18
+
19
+ def ==(other)
20
+ @location == other.instance_eval { @location }
21
+ end
22
+
23
+ def hash
24
+ @location.hash
25
+ end
26
+
27
+ def eql?(other)
28
+ @location.eql?(other.instance_eval { @location })
29
+ end
30
+
31
+ def inspect
32
+ @location
33
+ end
34
+
35
+ def find_mod_dependencies(consts)
36
+ visitor = DependencyVisitor.new(consts: consts)
37
+ visit_ast(visitor)
38
+ visitor.dependencies
39
+ end
40
+
41
+ def find_consts
42
+ visitor = ConstVisitor.new
43
+ visit_ast(visitor)
44
+ visitor.consts
45
+ end
46
+
47
+ def location?(glob)
48
+ @location == glob
49
+ end
50
+
51
+ private
52
+
53
+ SYNTAX_ERROR = "error"
54
+
55
+ def ast
56
+ @ast ||= create_ast
57
+ end
58
+
59
+ def create_ast
60
+ begin
61
+ root = Parser::CurrentRuby.parse(code)
62
+ rescue Parser::SyntaxError
63
+ return SYNTAX_ERROR
64
+ end
65
+ root.nil? ? SYNTAX_ERROR : root
66
+ end
67
+
68
+ def code
69
+ @code ||= @code_loader.call
70
+ end
71
+
72
+ def visit_ast(visitor)
73
+ return if ast == SYNTAX_ERROR
74
+
75
+ SourceProcessor.new(location: @location).process(ast, visitor)
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,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
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "optparse"
4
+ require "mudguard/application/application"
5
+ require_relative "notification_adapter"
6
+
7
+ module Mudguard
8
+ module Infrastructure
9
+ module Cli
10
+ # Parses the cli arguments
11
+ class Controller
12
+ def initialize(view:) # rubocop:disable Metrics/MethodLength
13
+ @cmd = :analyse
14
+ @view = view
15
+ @display_opts = {
16
+ view: view,
17
+ compressed: false
18
+ }
19
+ @parser = ::OptionParser.new do |opts|
20
+ opts.banner = "Usage: mudguard [options] [directory]"
21
+ opts.on("-h", "--help", "Prints this help") do
22
+ @cmd = :help
23
+ end
24
+ opts.on("-p", "--print", "Prints all allowed dependencies") do
25
+ @cmd = :print_allowed
26
+ end
27
+ opts.on("-c", "--compressed", "Omits printing the same dependency more than once") do
28
+ @display_opts[:compressed] = true
29
+ end
30
+ end
31
+ end
32
+
33
+ def parse!(argv) # rubocop:disable Metrics/MethodLength
34
+ directories = @parser.parse!(argv)
35
+
36
+ case @cmd
37
+ when :print_allowed
38
+ print_allowed_dependencies(directories)
39
+ when :help
40
+ help
41
+ when :analyse
42
+ check_dependencies(directories)
43
+ else
44
+ raise StandardError, "unknown command #{@cmd}"
45
+ end
46
+ end
47
+
48
+ private
49
+
50
+ def help
51
+ @view.print(@parser.to_s)
52
+ true
53
+ end
54
+
55
+ def check_dependencies(directories)
56
+ yield_directories(directories) do |directory, notification|
57
+ Mudguard::Application.check(directory, notification)
58
+ end
59
+ end
60
+
61
+ def print_allowed_dependencies(directories)
62
+ yield_directories(directories) do |directory, notification|
63
+ Mudguard::Application.print_allowed_dependencies(directory, notification)
64
+ true
65
+ end
66
+ end
67
+
68
+ def yield_directories(directories)
69
+ notification = NotificationAdapter.new(**@display_opts)
70
+ directories = [Dir.pwd] if directories.empty?
71
+ directories.all? do |directory|
72
+ yield directory, notification
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mudguard
4
+ module Infrastructure
5
+ module Cli
6
+ # Forwards Notification to the view for printing
7
+ class NotificationAdapter
8
+ def initialize(view:, compressed: false)
9
+ @view = view
10
+ @compressed = compressed
11
+ @printed_texts = Set.new
12
+ end
13
+
14
+ def add(location, text)
15
+ text = if location.nil? || @compressed
16
+ text
17
+ else
18
+ "#{location} #{text}"
19
+ end
20
+ @view.print(text) unless @printed_texts.include?(text)
21
+ @printed_texts << text
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mudguard
4
+ module Infrastructure
5
+ module Cli
6
+ # Handles user input
7
+ class View
8
+ def print(text)
9
+ puts text
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "yaml"
4
+ require_relative "../../domain/policies"
5
+ require_relative "../../domain/error"
6
+
7
+ module Mudguard
8
+ module Infrastructure
9
+ module Persistence
10
+ # A file containing the Mudguard-Policies
11
+ class PolicyFile
12
+ class << self
13
+ def read(project_path)
14
+ policy_file = File.join(project_path, ".mudguard.yml")
15
+ policy_exists = File.exist?(policy_file)
16
+
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)
22
+ end
23
+
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
41
+ end
42
+ end
43
+ end
44
+ end
45
+ 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
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pathname"
4
+ require "mudguard/domain/source"
5
+
6
+ module Mudguard
7
+ module Infrastructure
8
+ module Persistence
9
+ # Provides access to all ruby-files of a project
10
+ class RubyFiles
11
+ class << self
12
+ def select(project_path, patterns: nil)
13
+ project_exists = Dir.exist?(project_path)
14
+
15
+ unless project_exists
16
+ raise Mudguard::Domain::Error, "expected project #{project_path} doesn't exists"
17
+ end
18
+
19
+ patterns = [File.join("**", "*.rb")] if patterns.nil?
20
+ enumerate_files(project_path, patterns)
21
+ end
22
+
23
+ private
24
+
25
+ def enumerate_files(project_path, patterns)
26
+ project_path_name = Pathname.new(project_path)
27
+ ruby_files = patterns.map { |p| File.join(project_path, p) }
28
+ Dir.glob(ruby_files).map do |f|
29
+ file_path_name = Pathname.new(f)
30
+ diff_path = file_path_name.relative_path_from(project_path_name).to_s
31
+ Mudguard::Domain::Source.new(location: File.join("./", diff_path),
32
+ code_loader: -> { File.read(f) })
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
39
+ 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.3"
4
+ VERSION = "0.1.8"
5
5
  end
@@ -12,7 +12,7 @@ Gem::Specification.new do |spec|
12
12
 
13
13
  spec.summary = "mudguard helps your ruby project not becoming a "\
14
14
  "'Big ball of mud'"
15
- spec.homepage = "https://gibhub.com/Enceradeira/mudguard"
15
+ spec.homepage = "https://github.com/Enceradeira/mudguard"
16
16
  spec.license = "MIT"
17
17
 
18
18
  spec.metadata["homepage_uri"] = spec.homepage
@@ -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.3
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-15 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
@@ -171,23 +185,36 @@ files:
171
185
  - Rakefile
172
186
  - exe/mudguard
173
187
  - lib/mudguard.rb
174
- - lib/mudguard/error.rb
175
- - lib/mudguard/policies.rb
176
- - lib/mudguard/policy_file.rb
177
- - lib/mudguard/ruby_analyser.rb
178
- - lib/mudguard/ruby_files.rb
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
192
+ - lib/mudguard/domain/dependency.rb
193
+ - lib/mudguard/domain/dependency_visitor.rb
194
+ - lib/mudguard/domain/error.rb
195
+ - lib/mudguard/domain/policies.rb
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
200
+ - lib/mudguard/infrastructure/cli/controller.rb
201
+ - lib/mudguard/infrastructure/cli/notification_adapter.rb
202
+ - lib/mudguard/infrastructure/cli/view.rb
203
+ - lib/mudguard/infrastructure/persistence/policy_file.rb
204
+ - lib/mudguard/infrastructure/persistence/project_repository.rb
205
+ - lib/mudguard/infrastructure/persistence/ruby_files.rb
206
+ - lib/mudguard/infrastructure/rake/task.rb
179
207
  - lib/mudguard/version.rb
180
- - lib/tasks/gem.rake
181
208
  - lib/tasks/rubocop.rake
182
209
  - lib/tasks/test.rake
183
210
  - mudguard.gemspec
184
211
  - pkg/mudguard-0.1.0.gem
185
- homepage: https://gibhub.com/Enceradeira/mudguard
212
+ homepage: https://github.com/Enceradeira/mudguard
186
213
  licenses:
187
214
  - MIT
188
215
  metadata:
189
- homepage_uri: https://gibhub.com/Enceradeira/mudguard
190
- source_code_uri: https://gibhub.com/Enceradeira/mudguard
216
+ homepage_uri: https://github.com/Enceradeira/mudguard
217
+ source_code_uri: https://github.com/Enceradeira/mudguard
191
218
  post_install_message:
192
219
  rdoc_options: []
193
220
  require_paths:
@@ -1,23 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "ruby_analyser"
4
-
5
- module Mudguard
6
- # Contains the policies to be enforced
7
- class Policies
8
- def initialize(policies: [])
9
- @policies = policies.map { |l| l.gsub(/\s/, "") }.map { |p| /^#{p}/ }
10
- end
11
-
12
- def check(sources)
13
- sources.all? do |source|
14
- dependencies = RubyAnalyser.find_mod_dependencies(source)
15
- dependencies.all? do |d|
16
- @policies.any? do |p|
17
- d.match?(p)
18
- end
19
- end
20
- end
21
- end
22
- end
23
- end
@@ -1,17 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "policies"
4
- require_relative "error"
5
-
6
- module Mudguard
7
- # A file containing the Mudguard-Policies
8
- class PolicyFile
9
- def self.read(policy_file)
10
- policy_exists = File.exist?(policy_file)
11
-
12
- raise Error, "expected policy file #{policy_file} doesn't exists" unless policy_exists
13
-
14
- Policies.new(policies: File.readlines(policy_file))
15
- end
16
- end
17
- end
@@ -1,74 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "parser/current"
4
-
5
- module Mudguard
6
- # Analyses Ruby-Source and returns extracted information
7
- class RubyAnalyser
8
- class << self
9
- def find_mod_dependencies(source)
10
- begin
11
- root = Parser::CurrentRuby.parse(source)
12
- rescue Parser::SyntaxError
13
- return []
14
- end
15
- return [] if root.nil?
16
-
17
- process(root)
18
- end
19
-
20
- private
21
-
22
- def process(node, module_name = "")
23
- case node
24
- when type?(:module)
25
- process_module(node.children)
26
- when type?(:class)
27
- process_class(node.children, module_name)
28
- when type?(:const)
29
- ["#{module_name}->#{find_const_name(node.children)}"]
30
- else
31
- ignore_and_continue(node, module_name)
32
- end
33
- end
34
-
35
- def ignore_and_continue(node, module_name)
36
- case node
37
- when children?
38
- node.children.flat_map { |c| process(c, module_name) }
39
- else
40
- []
41
- end
42
- end
43
-
44
- def process_module(children)
45
- module_name = find_const_name(children[0].children)
46
- process(children[1], module_name)
47
- end
48
-
49
- def process_class(children, module_name)
50
- process(children[1], module_name)
51
- end
52
-
53
- def find_const_name(children)
54
- return nil if children.nil?
55
-
56
- module_name = find_const_name(children[0]&.children)
57
- const_name = children[1].to_s
58
- if module_name.nil?
59
- const_name
60
- else
61
- "#{module_name}::#{const_name}"
62
- end
63
- end
64
-
65
- def children?
66
- ->(n) { n.respond_to?(:children) }
67
- end
68
-
69
- def type?(type)
70
- ->(n) { n.respond_to?(:type) && n.type == type }
71
- end
72
- end
73
- end
74
- end
@@ -1,17 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Mudguard
4
- # Provides access to all ruby-files of a project
5
- class RubyFiles
6
- class << self
7
- def all(project_path)
8
- project_exists = Dir.exist?(project_path)
9
-
10
- raise Error, "expected project #{project_path} doesn't exists" unless project_exists
11
-
12
- ruby_files = File.join(project_path, "**", "*.rb")
13
- Dir.glob(ruby_files).lazy
14
- end
15
- end
16
- end
17
- end
@@ -1,15 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "rspec/core/rake_task"
4
- require "mudguard/version"
5
-
6
- namespace :gem do
7
- desc "Publishes the gem on rubygems.org"
8
- task publish: "gem:build" do
9
- exec("gem push mudguard-#{Mudguard::VERSION}.gem")
10
- end
11
- desc "Builds the gem"
12
- task :build do
13
- exec("gem build mudguard.gemspec")
14
- end
15
- end