mudguard 0.1.4 → 0.2.0

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