mudguard 0.1.6 → 0.2.2

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: 93199ae375f4801546571b433f77529637f7762d6e6009e849fb2a2cfba80f1f
4
- data.tar.gz: 6d17e05854f9196715323ec761e96888116fdb6d2ea9b7b1a0c3d45ccc7a0aec
3
+ metadata.gz: 9eb5bc544f7c9cfd9d472be7531d3dcf0c9f66ef53b30333ffa60fdd09092e6a
4
+ data.tar.gz: 4e3a47b2512035f09678e8b1b3670a90c1f150f995ca57b9b13ce569cadc6eda
5
5
  SHA512:
6
- metadata.gz: a14b61b6b2ab350e98ac006340ea5662bb2bd695a6c5bc3d868e3ce7fae8511d4675a05929f4fc6210c85c2e05c95e35912569aa2f0f105e30efb4c47982fbd7
7
- data.tar.gz: 0b032f68f9bd97cf9322b63f29d2260b68ad3a78b34e603311884d17260d56d234f04c31e2d29714a7e997e1005c75051efacc19b2d371eb08596079d92e1c89
6
+ metadata.gz: 3e0818bd960ef30419b87e98169e7dbfe791ae1c5a9be458100c21a4a2dcc9cb5d615a9449d929ac1d0b45bc23059d4e218950df0fbdc737064f9a8198d0ddd6
7
+ data.tar.gz: d099a3fe5e9f98a7c6cc02882d73177d142dc77bbc39d28d48d550ff1967082f60c0b4afbcdf021c574fa506c581bf28f6b465b74e3d9262411eeedd513f4e57
data/.gitignore CHANGED
@@ -16,3 +16,8 @@
16
16
 
17
17
  # Ignore pkg (gem dir)
18
18
  /pkg
19
+ /tmp
20
+
21
+ # Following file is dynamically created while running tests
22
+ /spec/lib/infrastructure/persistence/policy_file_project/.mudguard.yml
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.6)
4
+ mudguard (0.2.2)
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
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  rspec_options = {
4
- cmd: "rspec",
4
+ cmd: "bundle exec rspec",
5
5
  notification: false,
6
6
  failed_mode: :focus
7
7
  }
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,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "../infrastructure/persistence/policy_file"
4
- require_relative "../infrastructure/persistence/ruby_files"
3
+ require_relative "../infrastructure/persistence/project_repository"
4
+ require_relative "../domain/policies"
5
5
 
6
6
  # Contains methods to check if your project is a bit muddy
7
7
  module Mudguard
@@ -9,10 +9,24 @@ module Mudguard
9
9
  module Application
10
10
  class << self
11
11
  def check(project_path, notification)
12
- policy_file_path = File.expand_path("MudguardFile", project_path)
13
- policies = Infrastructure::Persistence::PolicyFile.read(policy_file_path)
12
+ create_policies(project_path) do |policies|
13
+ policies.check(notification)
14
+ end
15
+ end
16
+
17
+ def print_allowed_dependencies(project_path, notification)
18
+ create_policies(project_path) do |policies|
19
+ policies.print_allowed_dependencies(notification)
20
+ end
21
+ end
22
+
23
+ private
14
24
 
15
- policies.check(Infrastructure::Persistence::RubyFiles.all(project_path), notification)
25
+ def create_policies(project_path)
26
+ repo = Infrastructure::Persistence::ProjectRepository
27
+ source_policies = repo.load_source_policies(project_path)
28
+ policies = Domain::Policies.new(source_policies: source_policies)
29
+ yield policies
16
30
  end
17
31
  end
18
32
  end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mudguard
4
+ module Domain
5
+ # Transforms AST-Nodes into Strings denoting consts
6
+ class ConstVisitor
7
+ def initialize
8
+ @consts = []
9
+ end
10
+
11
+ attr_reader :consts
12
+
13
+ def visit_dependency(_, __, ___); end # rubocop:disable Naming/MethodParameterName
14
+
15
+ def visit_const_declaration(_location, const_name, module_name)
16
+ @consts << "#{module_name}#{const_name}"
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "error"
4
+
5
+ module Mudguard
6
+ module Domain
7
+ # Knows all constants of the project
8
+ class Consts
9
+ def initialize(sources:)
10
+ @consts = sources
11
+ .flat_map(&:find_consts)
12
+ .each_with_object(Hash.new { |h, k| h[k] = {} }) do |c, a|
13
+ path = split_hierarchy(c)
14
+ const_name = path.last
15
+ module_names = path.take(path.count - 1)
16
+ sub_module = module_names.reduce(a) { |h, m| h[m] }
17
+ sub_module[const_name] = {} unless sub_module.key?(const_name)
18
+ end
19
+ end
20
+
21
+ def resolve(module_name, const_name)
22
+ raise Error, "const_name is undefined" if const_name.empty?
23
+
24
+ path = split_hierarchy(module_name)
25
+ if module_name.empty?
26
+ # not in a module therefor const can only be defined in the root module (::)
27
+ qualified_path(const_name)
28
+ else
29
+ # analyse module hierarchy to find fully qualified const name
30
+ # resolve_in_modules(const_name, path)
31
+ const_path = const_name.split(SEPARATOR).drop(1)
32
+ find_const_deeper("", path, @consts, const_path) || qualified_path(const_name)
33
+ end
34
+ end
35
+
36
+ private
37
+
38
+ SEPARATOR = "::"
39
+
40
+ def find_const_deeper(current_module, remaining_modules, consts, const_path)
41
+ return if consts.nil?
42
+
43
+ if remaining_modules.any?
44
+ # Move deeper toward target module
45
+ next_module = remaining_modules.first
46
+ next_remaining = remaining_modules.drop(1)
47
+ next_consts = consts[next_module]
48
+ found_const = find_const_deeper(next_module, next_remaining, next_consts, const_path)
49
+ return join_path(current_module, found_const) if found_const
50
+ end
51
+
52
+ find_const(current_module, consts, const_path)
53
+ end
54
+
55
+ def find_const(current_module, consts, const_path)
56
+ const_name = const_path.first
57
+ if const_path.length == 1 && consts.key?(const_name)
58
+ # const defined in current_module
59
+ return join_path(current_module, const_name)
60
+ end
61
+
62
+ # backward search (along const_path only)
63
+ next_path = const_path.drop(1)
64
+ found_const = find_const_deeper(const_name, next_path, consts[const_name], next_path)
65
+ found_const ? join_path(current_module, found_const) : nil
66
+ end
67
+
68
+ def join_path(module_name, const_name)
69
+ "#{module_name}#{SEPARATOR}#{const_name}"
70
+ end
71
+
72
+ def qualified_path(const_name)
73
+ const_name =~ /^#{SEPARATOR}/ ? const_name : "#{SEPARATOR}#{const_name}"
74
+ end
75
+
76
+ def split_hierarchy(module_name)
77
+ module_name.split(SEPARATOR).drop(1)
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "texts"
4
+
5
+ module Mudguard
6
+ module Domain
7
+ # Executes operation on a set of dependencies
8
+ class Dependencies
9
+ include Texts
10
+
11
+ def initialize(policies:, notification:)
12
+ @policies = policies.map { |p| /^#{p}/x }
13
+ @notification = notification
14
+ end
15
+
16
+ def check(dependencies)
17
+ select_dependencies(dependencies) do |dependency, is_allowed|
18
+ add_message(dependency, dependency_not_allowed(dependency.dependency)) unless is_allowed
19
+ !is_allowed
20
+ end
21
+ end
22
+
23
+ def print_allowed(dependencies)
24
+ select_dependencies(dependencies) do |dependency, is_allowed|
25
+ add_message(dependency, dependency_allowed(dependency.dependency)) if is_allowed
26
+ is_allowed
27
+ end
28
+ end
29
+
30
+ private
31
+
32
+ def add_message(dependency, message)
33
+ @notification.add(dependency.location, message)
34
+ end
35
+
36
+ def select_dependencies(dependencies)
37
+ dependencies.select do |dependency|
38
+ yield dependency, dependency_allowed?(dependency)
39
+ end.count
40
+ end
41
+
42
+ def dependency_allowed?(dependency)
43
+ @policies.any? { |p| dependency.match(p) }
44
+ end
45
+ end
46
+ end
47
+ end
@@ -4,6 +4,8 @@ module Mudguard
4
4
  module Domain
5
5
  # A Dependency between Modules
6
6
  class Dependency
7
+ attr_reader :location, :dependency
8
+
7
9
  def initialize(location: nil, dependency:)
8
10
  @location = location
9
11
  @dependency = dependency
@@ -13,10 +15,6 @@ module Mudguard
13
15
  "{#{@location}, #{@dependency}}"
14
16
  end
15
17
 
16
- def to_s
17
- "#{@location} #{@dependency}"
18
- end
19
-
20
18
  def match(policy)
21
19
  @dependency.match(policy)
22
20
  end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mudguard
4
+ module Domain
5
+ # Transforms AST-Nodes into Dependencies
6
+ class DependencyVisitor
7
+ def initialize(consts:)
8
+ @consts = consts
9
+ @dependencies = []
10
+ end
11
+
12
+ attr_reader :dependencies
13
+
14
+ def visit_dependency(location, const_name, module_name)
15
+ qualified_const_name = @consts.resolve(module_name, const_name)
16
+ return [] unless qualified_const_name&.include?("::")
17
+
18
+ dependency = if module_name.empty?
19
+ "->#{qualified_const_name}"
20
+ else
21
+ "#{module_name}->#{qualified_const_name}"
22
+ end
23
+
24
+ @dependencies << Dependency.new(location: location, dependency: dependency)
25
+ end
26
+
27
+ # rubocop:disable Naming/MethodParameterName
28
+ def visit_const_declaration(_, __, ___); end
29
+ # rubocop:enable Naming/MethodParameterName
30
+ end
31
+ end
32
+ end
@@ -1,47 +1,48 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "dependencies"
4
+ require_relative "texts"
5
+ require_relative "consts"
6
+
3
7
  module Mudguard
4
8
  module Domain
5
9
  # Contains the policies to be enforced
6
10
  class Policies
7
- def initialize(policies: [])
8
- @policies = policies.map { |l| l.gsub(/\s/, "") }.map { |p| /^#{p}/ }
11
+ include Texts
12
+
13
+ def initialize(source_policies: [])
14
+ @source_policies = source_policies
9
15
  end
10
16
 
11
- def check(sources, notification)
12
- result = check_sources(sources, notification)
17
+ def check(notification)
18
+ result = analyse(:check, notification)
13
19
 
14
- count = result[:count]
15
- violations = result[:violations]
20
+ count = result[:sources_count]
21
+ violations = result[:analyser_count]
16
22
 
17
- files = pluralize("file", count)
18
- problems = pluralize("problem", violations)
19
- notification.add("#{count} #{files} inspected, #{violations} #{problems} detected")
23
+ notification.add(nil, summary(count, violations))
20
24
  violations.zero?
21
25
  end
22
26
 
23
- private
27
+ def print_allowed_dependencies(notification)
28
+ result = analyse(:print_allowed, notification)
24
29
 
25
- def pluralize(word, count)
26
- count == 1 ? word : "#{word}s"
27
- end
30
+ count = result[:sources_count]
31
+ violations = result[:analyser_count]
28
32
 
29
- def check_sources(sources, notification)
30
- sources.each_with_object(count: 0, violations: 0) do |source, result|
31
- result[:count] += 1
32
- dependencies = source.find_mod_dependencies
33
- result[:violations] += check_dependencies(dependencies, notification)
34
- end
33
+ notification.add(nil, dependency_summary(count, violations))
35
34
  end
36
35
 
37
- def check_dependencies(dependencies, notification)
38
- dependencies.reject { |d| check_dependency(d, notification) }.count
39
- end
36
+ private
40
37
 
41
- def check_dependency(dependency, notification)
42
- is_allowed = @policies.any? { |p| dependency.match(p) }
43
- notification.add("#{dependency} not allowed") unless is_allowed
44
- is_allowed
38
+ def analyse(method, notification)
39
+ consts = Consts.new(sources: @source_policies.map(&:source))
40
+ @source_policies.each_with_object(sources_count: 0, analyser_count: 0) do |sp, result|
41
+ analyser = Dependencies.new(policies: sp.policies, notification: notification)
42
+ dependencies = sp.source.find_mod_dependencies(consts)
43
+ result[:sources_count] += 1
44
+ result[:analyser_count] += analyser.send(method, dependencies)
45
+ end
45
46
  end
46
47
  end
47
48
  end
@@ -1,93 +1,85 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "parser/current"
4
- require "mudguard/domain/dependency"
3
+ orig = $VERBOSE
4
+ begin
5
+ $VERBOSE = nil # silence warning when ruby versions are not exactly matching
6
+ require "parser/current"
7
+ ensure
8
+ $VERBOSE = orig
9
+ end
10
+
11
+ require_relative "dependency"
12
+ require_relative "dependency_visitor"
13
+ require_relative "const_visitor"
14
+ require_relative "error"
15
+ require_relative "source_processor"
5
16
 
6
17
  module Mudguard
7
18
  module Domain
8
19
  # Represents a Ruby source file
9
20
  class Source
10
- def initialize(location: nil, code:)
11
- @code = code
21
+ def initialize(location:, code_loader: -> { "" })
22
+ @code_loader = code_loader
12
23
  @location = location
13
24
  end
14
25
 
15
26
  def ==(other)
16
- @code == other.instance_eval { @code } && @location == other.instance_eval { @location }
27
+ @location == other.instance_eval { @location }
17
28
  end
18
29
 
19
- def inspect
20
- @location
30
+ def hash
31
+ @location.hash
21
32
  end
22
33
 
23
- def find_mod_dependencies
24
- begin
25
- root = Parser::CurrentRuby.parse(@code)
26
- rescue Parser::SyntaxError
27
- return []
28
- end
29
- return [] if root.nil?
30
-
31
- process(root)
34
+ def eql?(other)
35
+ @location.eql?(other.instance_eval { @location })
32
36
  end
33
37
 
34
- private
35
-
36
- def process(node, module_name = "")
37
- case node
38
- when type?(:module)
39
- process_module(node.children)
40
- when type?(:class)
41
- process_class(node.children, module_name)
42
- when type?(:const)
43
- [create_dependency(module_name, node)]
44
- else
45
- ignore_and_continue(node, module_name)
46
- end
38
+ def inspect
39
+ @location
47
40
  end
48
41
 
49
- def create_dependency(module_name, node)
50
- dependency = "#{module_name}->#{find_const_name(node.children)}"
51
- location = "#{@location}:#{node.location.line}"
52
- Dependency.new(location: location, dependency: dependency)
42
+ def find_mod_dependencies(consts)
43
+ visitor = DependencyVisitor.new(consts: consts)
44
+ visit_ast(visitor)
45
+ visitor.dependencies
53
46
  end
54
47
 
55
- def ignore_and_continue(node, module_name)
56
- case node
57
- when children?
58
- node.children.flat_map { |c| process(c, module_name) }
59
- else
60
- []
61
- end
48
+ def find_consts
49
+ visitor = ConstVisitor.new
50
+ visit_ast(visitor)
51
+ visitor.consts
62
52
  end
63
53
 
64
- def process_module(children)
65
- module_name = find_const_name(children[0].children)
66
- process(children[1], module_name)
54
+ def location?(glob)
55
+ @location == glob
67
56
  end
68
57
 
69
- def process_class(children, module_name)
70
- process(children[1], module_name)
71
- end
58
+ private
72
59
 
73
- def find_const_name(children)
74
- return nil if children.nil?
60
+ SYNTAX_ERROR = "error"
75
61
 
76
- module_name = find_const_name(children[0]&.children)
77
- const_name = children[1].to_s
78
- if module_name.nil?
79
- const_name
80
- else
81
- "#{module_name}::#{const_name}"
62
+ def ast
63
+ @ast ||= create_ast
64
+ end
65
+
66
+ def create_ast
67
+ begin
68
+ root = Parser::CurrentRuby.parse(code)
69
+ rescue Parser::SyntaxError
70
+ return SYNTAX_ERROR
82
71
  end
72
+ root.nil? ? SYNTAX_ERROR : root
83
73
  end
84
74
 
85
- def children?
86
- ->(n) { n.respond_to?(:children) }
75
+ def code
76
+ @code ||= @code_loader.call
87
77
  end
88
78
 
89
- def type?(type)
90
- ->(n) { n.respond_to?(:type) && n.type == type }
79
+ def visit_ast(visitor)
80
+ return if ast == SYNTAX_ERROR
81
+
82
+ SourceProcessor.new(location: @location).process(ast, visitor)
91
83
  end
92
84
  end
93
85
  end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mudguard
4
+ module Domain
5
+ # Associates a source with it's policies
6
+ SourcePolicies = Struct.new(:source, :policies, keyword_init: true)
7
+ end
8
+ end
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mudguard
4
+ module Domain
5
+ # Processes the interesting parts of the Ast and forwards selected nodes to the visitors
6
+ class SourceProcessor
7
+ def initialize(location:)
8
+ @location = location
9
+ end
10
+
11
+ def process(node, visitor)
12
+ process_node(node, visitor, "")
13
+ end
14
+
15
+ private
16
+
17
+ def process_node(node, visitor, module_name) # rubocop:disable Metrics/MethodLength
18
+ case node
19
+ when type?(:module)
20
+ process_module(node, visitor, module_name)
21
+ when type?(:class)
22
+ process_module(node, visitor, module_name)
23
+ when type?(:const)
24
+ process_const(node, visitor, module_name)
25
+ when type?(:casgn)
26
+ process_const_assignment(node, visitor, module_name)
27
+ else
28
+ ignore_and_continue(node, visitor, module_name)
29
+ end
30
+ end
31
+
32
+ def process_const_assignment(node, visitor, module_name)
33
+ is_explicit, is_static, const_name = find_const_name(node.children)
34
+ return unless is_static
35
+
36
+ visitor.visit_const_declaration(describe_location(node), const_name,
37
+ is_explicit ? "" : module_name)
38
+ end
39
+
40
+ def process_const(node, visitor, module_name)
41
+ is_explicit, is_static, const_name = find_const_name(node.children)
42
+ return unless is_static
43
+
44
+ visitor.visit_dependency(describe_location(node), const_name,
45
+ is_explicit ? "" : module_name)
46
+ end
47
+
48
+ def describe_location(node)
49
+ "#{@location}:#{node.location.line}"
50
+ end
51
+
52
+ def ignore_and_continue(node, visitor, module_name)
53
+ return unless node.respond_to?(:children)
54
+
55
+ node.children.flat_map { |c| process_node(c, visitor, module_name) }
56
+ end
57
+
58
+ def process_module(node, visitor, module_name)
59
+ is_explicit, is_static, const_name = find_const_name(node.children[0].children)
60
+ return unless is_static
61
+
62
+ visitor.visit_const_declaration(describe_location(node), const_name, module_name)
63
+
64
+ module_name = "#{is_explicit ? '' : module_name}#{const_name}"
65
+ node.children.drop(1).reject(&:nil?).each do |child_node|
66
+ process_node(child_node, visitor, module_name)
67
+ end
68
+ end
69
+
70
+ def find_const_name(children)
71
+ return [false, nil] if children.nil? || children.empty?
72
+
73
+ first_child = children[0]
74
+ is_explicit, is_static = find_const_type(first_child)
75
+ first_child_children = first_child.respond_to?(:children) ? first_child.children : nil
76
+ _, __, module_name = find_const_name(first_child_children)
77
+ const_name = children[1].to_s
78
+ [is_explicit, is_static, "#{module_name}::#{const_name}"]
79
+ end
80
+
81
+ def find_const_type(first_child)
82
+ is_explicit = type?(:cbase).call(first_child)
83
+ is_static = is_explicit || first_child.nil? || type?(:const).call(first_child)
84
+ [is_explicit, is_static]
85
+ end
86
+
87
+ def type?(type)
88
+ ->(n) { n.respond_to?(:type) && n.type == type }
89
+ end
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mudguard
4
+ module Domain
5
+ # Builds texts to be displayed to a user
6
+ module Texts
7
+ def summary(file_count, violation_count)
8
+ files = pluralize("file", file_count)
9
+ "#{file_count} #{files} inspected, #{count(violation_count)}\
10
+ bad #{dependency(violation_count)} detected"
11
+ end
12
+
13
+ def dependency_not_allowed(dependency)
14
+ "#{dependency} not allowed"
15
+ end
16
+
17
+ def dependency_summary(file_count, dependency_count)
18
+ files = pluralize("file", file_count)
19
+ "#{file_count} #{files} inspected, #{count(dependency_count)}\
20
+ good #{dependency(dependency_count)} detected"
21
+ end
22
+
23
+ def dependency_allowed(dependency)
24
+ dependency.to_s
25
+ end
26
+
27
+ private
28
+
29
+ def dependency(count)
30
+ count <= 1 ? "dependency" : "dependencies"
31
+ end
32
+
33
+ def count(count)
34
+ count.zero? ? "no" : count.to_s
35
+ end
36
+
37
+ def pluralize(word, count)
38
+ count == 1 ? word : "#{word}s"
39
+ end
40
+ end
41
+ end
42
+ end
@@ -9,26 +9,68 @@ module Mudguard
9
9
  module Cli
10
10
  # Parses the cli arguments
11
11
  class Controller
12
- def initialize(view:)
12
+ def initialize(view:) # rubocop:disable Metrics/MethodLength
13
+ @cmd = :analyse
13
14
  @view = view
14
- @done = false
15
+ @display_opts = {
16
+ view: view,
17
+ compressed: false
18
+ }
15
19
  @parser = ::OptionParser.new do |opts|
16
20
  opts.banner = "Usage: mudguard [options] [directory]"
17
21
  opts.on("-h", "--help", "Prints this help") do
18
- view.print(opts.to_s)
19
- @done = true
22
+ @cmd = :help
23
+ end
24
+ opts.on("-p", "--print", "Prints all allowed dependencies") do
25
+ @cmd = :print_allowed
26
+ end
27
+ opts.on("-c", "--compressed", "Omits printing the same dependency more than once") do
28
+ @display_opts[:compressed] = true
20
29
  end
21
30
  end
22
31
  end
23
32
 
24
- def parse!(argv)
33
+ def parse!(argv) # rubocop:disable Metrics/MethodLength
25
34
  directories = @parser.parse!(argv)
26
35
 
27
- return true if @done
36
+ case @cmd
37
+ when :print_allowed
38
+ print_allowed_dependencies(directories)
39
+ when :help
40
+ help
41
+ when :analyse
42
+ check_dependencies(directories)
43
+ else
44
+ raise StandardError, "unknown command #{@cmd}"
45
+ end
46
+ end
47
+
48
+ private
49
+
50
+ def help
51
+ @view.print(@parser.to_s)
52
+ true
53
+ end
54
+
55
+ def check_dependencies(directories)
56
+ yield_directories(directories) do |directory, notification|
57
+ Mudguard::Application.check(directory, notification)
58
+ end
59
+ end
28
60
 
29
- notification = NotificationAdapter.new(view: @view)
61
+ def print_allowed_dependencies(directories)
62
+ yield_directories(directories) do |directory, notification|
63
+ Mudguard::Application.print_allowed_dependencies(directory, notification)
64
+ true
65
+ end
66
+ end
67
+
68
+ def yield_directories(directories)
69
+ notification = NotificationAdapter.new(**@display_opts)
30
70
  directories = [Dir.pwd] if directories.empty?
31
- directories.all? { |d| Mudguard::Application.check(d, notification) }
71
+ directories.all? do |directory|
72
+ yield directory, notification
73
+ end
32
74
  end
33
75
  end
34
76
  end
@@ -5,12 +5,20 @@ module Mudguard
5
5
  module Cli
6
6
  # Forwards Notification to the view for printing
7
7
  class NotificationAdapter
8
- def initialize(view:)
8
+ def initialize(view:, compressed: false)
9
9
  @view = view
10
+ @compressed = compressed
11
+ @printed_texts = Set.new
10
12
  end
11
13
 
12
- def add(text)
13
- @view.print(text)
14
+ def add(location, text)
15
+ text = if location.nil? || @compressed
16
+ text
17
+ else
18
+ "#{location} #{text}"
19
+ end
20
+ @view.print(text) unless @printed_texts.include?(text)
21
+ @printed_texts << text
14
22
  end
15
23
  end
16
24
  end
@@ -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,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "yaml"
3
4
  require_relative "../../domain/policies"
4
5
  require_relative "../../domain/error"
5
6
 
@@ -8,14 +9,36 @@ module Mudguard
8
9
  module Persistence
9
10
  # A file containing the Mudguard-Policies
10
11
  class PolicyFile
11
- def self.read(policy_file)
12
- policy_exists = File.exist?(policy_file)
12
+ class << self
13
+ def read(project_path)
14
+ policy_file = File.join(project_path, ".mudguard.yml")
15
+ policy_exists = File.exist?(policy_file)
13
16
 
14
- unless policy_exists
15
- raise Mudguard::Domain::Error, "expected policy file #{policy_file} doesn't exists"
17
+ unless policy_exists
18
+ template_file = File.join(__dir__, ".mudguard.template.yml")
19
+ FileUtils.cp(template_file, policy_file)
20
+ end
21
+
22
+ read_yml(policy_file)
16
23
  end
17
24
 
18
- Mudguard::Domain::Policies.new(policies: File.readlines(policy_file))
25
+ private
26
+
27
+ def read_yml(policy_file)
28
+ yaml_file = File.read(policy_file)
29
+ yaml = YAML.safe_load(yaml_file, [Symbol], [], policy_file) || {}
30
+ yaml.transform_values { |value| (value || []).map(&method(:unsymbolize)) }
31
+ rescue Psych::SyntaxError => e
32
+ raise Mudguard::Domain::Error, "#{policy_file} is invalid (#{e.message})"
33
+ end
34
+
35
+ def unsymbolize(dependency)
36
+ if dependency.is_a?(Symbol)
37
+ ":#{dependency}"
38
+ else
39
+ dependency
40
+ end
41
+ end
19
42
  end
20
43
  end
21
44
  end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "policy_file"
4
+ require_relative "ruby_files"
5
+ require_relative "../../domain/source_policies"
6
+
7
+ module Mudguard
8
+ module Infrastructure
9
+ module Persistence
10
+ # Provides access to the persisted source and policies
11
+ class ProjectRepository
12
+ class << self
13
+ def load_source_policies(project_path)
14
+ file = PolicyFile.read(project_path)
15
+ scopes = file.flat_map do |patterns, policies|
16
+ sources = RubyFiles.select(project_path, patterns: [patterns])
17
+ sources.flat_map { |s| { source: s, policies: policies } }
18
+ end
19
+
20
+ sources = scopes.group_by { |e| e[:source] }
21
+
22
+ sources.map do |source, group|
23
+ policies = group.flat_map { |r| r[:policies] }.uniq
24
+ Domain::SourcePolicies.new(source: source, policies: policies)
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -9,26 +9,28 @@ module Mudguard
9
9
  # Provides access to all ruby-files of a project
10
10
  class RubyFiles
11
11
  class << self
12
- def all(project_path)
12
+ def select(project_path, patterns: nil)
13
13
  project_exists = Dir.exist?(project_path)
14
14
 
15
15
  unless project_exists
16
16
  raise Mudguard::Domain::Error, "expected project #{project_path} doesn't exists"
17
17
  end
18
18
 
19
- enumerate_files(project_path)
19
+ patterns = [File.join("**", "*.rb")] if patterns.nil?
20
+ enumerate_files(project_path, patterns)
20
21
  end
21
22
 
22
23
  private
23
24
 
24
- def enumerate_files(project_path)
25
+ def enumerate_files(project_path, patterns)
25
26
  project_path_name = Pathname.new(project_path)
26
- ruby_files = File.join(project_path, "**", "*.rb")
27
- Dir.glob(ruby_files).map do |f|
27
+ ruby_files = patterns.map { |p| File.join(project_path, p) }
28
+ Dir.glob(ruby_files).select { |f| File.file?(f) }.map do |f|
28
29
  file_path_name = Pathname.new(f)
29
30
  diff_path = file_path_name.relative_path_from(project_path_name).to_s
30
- Mudguard::Domain::Source.new(location: File.join("./", diff_path), code: File.read(f))
31
- end.lazy
31
+ Mudguard::Domain::Source.new(location: File.join("./", diff_path),
32
+ code_loader: -> { File.read(f) })
33
+ end
32
34
  end
33
35
  end
34
36
  end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rake"
4
+ require "rake/tasklib"
5
+ require_relative "../../application/application"
6
+ require_relative "../../infrastructure/cli/notification_adapter"
7
+ require_relative "../../infrastructure/cli/view"
8
+
9
+ module Mudguard
10
+ module Infrastructure
11
+ module Rake
12
+ # Provides Mudguard Rake Tasks
13
+ class Task < ::Rake::TaskLib
14
+ def initialize(project_dir: Dir.pwd)
15
+ @project_dir = project_dir
16
+
17
+ desc "Run Mudguard"
18
+ task(:mudguard) do
19
+ view = Mudguard::Infrastructure::Cli::View.new
20
+ notification = Mudguard::Infrastructure::Cli::NotificationAdapter.new(view: view)
21
+ ok = Application.check(@project_dir, notification)
22
+ abort unless ok
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Mudguard
4
- VERSION = "0.1.6"
4
+ VERSION = "0.2.2"
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.6
4
+ version: 0.2.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jorg Jenni
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2020-03-28 00:00:00.000000000 Z
11
+ date: 2021-01-09 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
@@ -160,6 +174,7 @@ extra_rdoc_files: []
160
174
  files:
161
175
  - ".envrc"
162
176
  - ".gitignore"
177
+ - ".mudguard.yml"
163
178
  - ".rubocop.yml"
164
179
  - ".ruby-version"
165
180
  - CODE_OF_CONDUCT.md
@@ -172,26 +187,36 @@ files:
172
187
  - exe/mudguard
173
188
  - lib/mudguard.rb
174
189
  - lib/mudguard/application/application.rb
190
+ - lib/mudguard/domain/const_visitor.rb
191
+ - lib/mudguard/domain/consts.rb
192
+ - lib/mudguard/domain/dependencies.rb
175
193
  - lib/mudguard/domain/dependency.rb
194
+ - lib/mudguard/domain/dependency_visitor.rb
176
195
  - lib/mudguard/domain/error.rb
177
196
  - lib/mudguard/domain/policies.rb
178
197
  - lib/mudguard/domain/source.rb
198
+ - lib/mudguard/domain/source_policies.rb
199
+ - lib/mudguard/domain/source_processor.rb
200
+ - lib/mudguard/domain/texts.rb
179
201
  - lib/mudguard/infrastructure/cli/controller.rb
180
202
  - lib/mudguard/infrastructure/cli/notification_adapter.rb
181
203
  - lib/mudguard/infrastructure/cli/view.rb
204
+ - lib/mudguard/infrastructure/persistence/.mudguard.template.yml
182
205
  - lib/mudguard/infrastructure/persistence/policy_file.rb
206
+ - lib/mudguard/infrastructure/persistence/project_repository.rb
183
207
  - lib/mudguard/infrastructure/persistence/ruby_files.rb
208
+ - lib/mudguard/infrastructure/rake/task.rb
184
209
  - lib/mudguard/version.rb
185
210
  - lib/tasks/rubocop.rake
186
211
  - lib/tasks/test.rake
187
212
  - mudguard.gemspec
188
213
  - pkg/mudguard-0.1.0.gem
189
- homepage: https://gibhub.com/Enceradeira/mudguard
214
+ homepage: https://github.com/Enceradeira/mudguard
190
215
  licenses:
191
216
  - MIT
192
217
  metadata:
193
- homepage_uri: https://gibhub.com/Enceradeira/mudguard
194
- source_code_uri: https://gibhub.com/Enceradeira/mudguard
218
+ homepage_uri: https://github.com/Enceradeira/mudguard
219
+ source_code_uri: https://github.com/Enceradeira/mudguard
195
220
  post_install_message:
196
221
  rdoc_options: []
197
222
  require_paths: