mudguard 0.1.6 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 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: