mudguard 0.1.3 → 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: '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