mudguard 0.1.4 → 0.2.0

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.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +8 -0
  3. data/.mudguard.yml +6 -0
  4. data/.rubocop.yml +2 -0
  5. data/Gemfile.lock +25 -23
  6. data/Guardfile +1 -1
  7. data/README.md +62 -9
  8. data/exe/mudguard +5 -12
  9. data/lib/mudguard.rb +2 -17
  10. data/lib/mudguard/application/application.rb +33 -0
  11. data/lib/mudguard/domain/const_visitor.rb +20 -0
  12. data/lib/mudguard/domain/consts.rb +81 -0
  13. data/lib/mudguard/domain/dependencies.rb +47 -0
  14. data/lib/mudguard/domain/dependency.rb +28 -0
  15. data/lib/mudguard/domain/dependency_visitor.rb +32 -0
  16. data/lib/mudguard/{error.rb → domain/error.rb} +3 -1
  17. data/lib/mudguard/domain/policies.rb +49 -0
  18. data/lib/mudguard/domain/source.rb +79 -0
  19. data/lib/mudguard/domain/source_policies.rb +8 -0
  20. data/lib/mudguard/domain/source_processor.rb +92 -0
  21. data/lib/mudguard/domain/texts.rb +42 -0
  22. data/lib/mudguard/infrastructure/cli/controller.rb +78 -0
  23. data/lib/mudguard/infrastructure/cli/notification_adapter.rb +26 -0
  24. data/lib/mudguard/infrastructure/cli/view.rb +14 -0
  25. data/lib/mudguard/infrastructure/persistence/.mudguard.template.yml +6 -0
  26. data/lib/mudguard/infrastructure/persistence/policy_file.rb +46 -0
  27. data/lib/mudguard/infrastructure/persistence/project_repository.rb +31 -0
  28. data/lib/mudguard/infrastructure/persistence/ruby_files.rb +39 -0
  29. data/lib/mudguard/infrastructure/rake/task.rb +28 -0
  30. data/lib/mudguard/version.rb +1 -1
  31. data/mudguard.gemspec +2 -1
  32. metadata +40 -11
  33. data/lib/mudguard/policies.rb +0 -23
  34. data/lib/mudguard/policy_file.rb +0 -17
  35. data/lib/mudguard/ruby_analyser.rb +0 -74
  36. data/lib/mudguard/ruby_files.rb +0 -17
  37. data/lib/tasks/gem.rake +0 -15
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e84d450c1adfd300b6be1e11244dd8eec43bc4ef2dffdd090974290591bce06f
4
- data.tar.gz: c36b94a49c130dc29b6ce80befc227121348c53b977a88451337bc6987b5a0fa
3
+ metadata.gz: 3a7b56ba51f10b0d6d32fcb120599bc145b275076b30786b6955285f5da26668
4
+ data.tar.gz: ca75c47ff5f80584407bb0da9d56b64f7c9e1e65cc8f7fc8b377ea0e8d83fefa
5
5
  SHA512:
6
- metadata.gz: 4db49720f6381c3c67a7e2e669235e070d8ed2cb650f48f41752dd95a243da43dc1bbc7bec7947828fd40c7abf3c7dc786c366cd04efb026d2c732cc5908d8d2
7
- data.tar.gz: 84291c975dce9b7b893ae831dcfc54cafe16313d17b86f590d27a529b231fe4e1c2a4683455746c7a5143bbf880fe39f17ffb7be16954a63647eb98bda5a22ad
6
+ metadata.gz: e9bd40c81779ed70c331c0d445b735edb3e561967e65fce1dc228a62ad86fd9298dfaa990d3bded3faec6191b5821204466c9ed29cdf89d12e9eee1678884d81
7
+ data.tar.gz: 427d86d72d87f73d11dedfbb7a35d42ead0145919bf85e32dd5c9055bc977a2dadb9be668f76a081e61f54510a6ebb4aa66e7f4b11746da1a222efde3ad5b760
data/.gitignore CHANGED
@@ -13,3 +13,11 @@
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
23
+ /spec/test_projects/project_without_mudguard_file/.mudguard.yml
@@ -0,0 +1,6 @@
1
+ './**/*.rb':
2
+ # uncomment following lines to allow all dependencies
3
+ # - .*
4
+
5
+ # all code can depend on modules or constants defined in same module
6
+ - ^(::.+)(::[^:]+)* -> \1(::[^:]+)*$
@@ -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.4)
4
+ mudguard (0.2.0)
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
@@ -12,5 +12,5 @@ guard "rspec", rspec_options do
12
12
  watch(%r{^lib/mudguard/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" }
13
13
  watch(%r{^lib/mudguard/(.+)\.rb$}) { |m| Dir.glob("spec/lib/#{m[1]}_*.rb") }
14
14
  watch("spec/spec_helper.rb") { "spec" }
15
- watch(%r{^exe\/mudguard$}) { |_| "spec/bin/mudguard_spec.rb" }
15
+ watch(%r{^exe\/mudguard$}) { |_| "spec/exe/mudguard_spec.rb" }
16
16
  end
data/README.md CHANGED
@@ -1,10 +1,16 @@
1
1
  # Mudguard
2
2
 
3
- Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/mudguard`. To experiment with that code, run `bin/console` for an interactive prompt.
3
+ **Mudguard** is a Ruby static code analyzer that investigates the coupling of modules in your source code. Mudguard
4
+ prevents your code from becoming a [Big ball of mud](http://www.laputan.org/mud/) and helps implementing and
5
+ maintaining an intended design.
4
6
 
5
- TODO: Delete this and the text above, and describe your gem
7
+ Mudguard is inspired by
8
+ * [Rubocop](https://github.com/rubocop-hq/rubocop)
9
+ * Clean Architecture by Robert C. Martin (ISBN-13: 978-0-13-449416-6)
10
+ * having seen many larger code bases in a sorrow state
11
+ * my inability to efficiently break up application code into gems or rails engines
6
12
 
7
- ## Installation
13
+ # Installation
8
14
 
9
15
  Add this line to your application's Gemfile:
10
16
 
@@ -20,19 +26,66 @@ Or install it yourself as:
20
26
 
21
27
  $ gem install mudguard
22
28
 
23
- ## Usage
29
+ ## Quickstart
30
+ ```
31
+ $ cd my/cool/ruby/project
32
+ $ mudguard
33
+ ```
34
+
35
+ Mudguard creates on it's first run a file called **.mudguard.yml** which is used to configure **design policies**
36
+ governing your code. A .mudguard.yml could look like:
37
+ ```
38
+ './**/*.rb': # all code
39
+ # all code can depend on siblings in same module or class
40
+ - ^(::.+)(::[^:]+)* -> \1(::[^:]+)*$
41
+ # all code can depend on certain types
42
+ - .* -> ::(String|SecureRandom|Container|Time|Date|JSON|Object|Hash)$
43
+
44
+ # in all code module Application can depend on module Domain
45
+ - ^::Application(::[^:]+)* -> ::(Domain)(::[^:]+)*$
46
+ # in all code module Infrastructure can depend on module Application
47
+ - ^::Infrastructure(::[^:]+)* -> ::(Application)(::[^:]+)*$
48
+
49
+ 'spec/**/*.rb': # spec code
50
+ # Only in test code can we use RSpec, Timecop and Exception
51
+ - .* -> ::(RSpec|Timecop|Exception)$
52
+ ```
53
+
54
+ ## The .mudguard.yml
55
+ The .mudguard.yml defines scopes and policies. A policy defines which dependencies are inside that scope permissible:
56
+ ```
57
+ scope1:
58
+ - policy 1
59
+ - policy 2
60
+ scope2:
61
+ - policy 3
62
+ ```
63
+ The **scope** is a [glob-pattern](https://en.wikipedia.org/wiki/Glob_(programming)) and defines to which files a set
64
+ of policies apply. For example a value `lib/docker/**/*.rb` defines a scope containing all Ruby files inside
65
+ folder lib/docker.
24
66
 
25
- TODO: Write usage instructions here
67
+ A **policy** is a [regular expression](https://ruby-doc.org/core-2.5.1/Regexp.html) matching one or a set of
68
+ dependencies that are permissible inside that scope. Mudguard represents a **dependency** as a symbol in form
69
+ of `X -> Y` meaning "X depends on Y". See following examples:
26
70
 
27
- ## Development
71
+ | Policy | matched Dependency| Explanation |
72
+ | --- | --- | --- |
73
+ | `^::A -> ::B$` | `::A -> ::B` |Module A can depend on module or constant B |
74
+ | `^::Infrastructure -> ::Rails$` | `::Infrastructure -> ::Rails` |Module Infrastructure can depend on Rails |
75
+ | `^::Infrastructure(::[^:]+)* -> ::ActiveRecord$` | `::Infrastructure::Logger -> ::ActiveRecord` or `::Infrastructure::Persistence -> ::ActiveRecord`|Module Infrastructure and its submodules can depend on ActiveRecord |
76
+ | `.* -> ::Exception$` | `::Api::Gateway -> ::Exception` or `::Logger -> ::Gateway` |Any module can depend on class Exception |
28
77
 
29
- After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
78
+ Any dependency for which Mudguard doesn't find a matching policy is reported as an impermissible dependency.
30
79
 
31
- To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
80
+ ## Outlook
81
+ Using regular expressions as policies is very flexible but difficult to use. Furthermore certain patterns used in
82
+ the regular expressions do repeat. It might be useful to replace regular expressions in future with a specific language
83
+ to describe permissible dependencies. Any thoughts on that?
32
84
 
33
85
  ## Contributing
34
86
 
35
- Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/mudguard. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
87
+ Bug reports and pull requests are welcome on GitHub at https://github.com/Enceradeira/mudguard.
88
+ This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
36
89
 
37
90
  ## License
38
91
 
@@ -1,20 +1,13 @@
1
1
  #!/usr/bin/env ruby
2
2
  # frozen_string_literal: true
3
3
 
4
- require "mudguard"
5
- require "optparse"
4
+ require "mudguard/infrastructure/cli/view"
5
+ require "mudguard/infrastructure/cli/controller"
6
6
 
7
7
  # Checks the dependencies of a ruby project
8
8
  module Mudguard
9
- parser = OptionParser.new do |opts|
10
- opts.banner = "Usage: mudguard [options] [directory]"
11
- opts.on("-h", "--help", "Prints this help") do
12
- puts opts
13
- exit
14
- end
15
- end
16
-
17
- directories = parser.parse!
18
- ok = directories.all?(&Mudguard.method(:check))
9
+ view = Mudguard::Infrastructure::Cli::View.new
10
+ parser = Mudguard::Infrastructure::Cli::Controller.new(view: view)
11
+ ok = parser.parse!(ARGV)
19
12
  exit(ok ? 0 : 1)
20
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