maximus 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +22 -0
  3. data/Gemfile +4 -0
  4. data/LICENSE.txt +22 -0
  5. data/README.md +66 -0
  6. data/Rakefile +2 -0
  7. data/bin/maximus +15 -0
  8. data/lib/maximus.rb +25 -0
  9. data/lib/maximus/cli.rb +106 -0
  10. data/lib/maximus/config/.jshintignore +2 -0
  11. data/lib/maximus/config/jshint.json +9 -0
  12. data/lib/maximus/config/phantomas.json +4 -0
  13. data/lib/maximus/config/phantomas_urls.yaml +1 -0
  14. data/lib/maximus/config/rubocop.yml +1007 -0
  15. data/lib/maximus/config/scsslint.yml +58 -0
  16. data/lib/maximus/config/stylestats.json +30 -0
  17. data/lib/maximus/config/wraith.yaml +56 -0
  18. data/lib/maximus/config/wraith/casper.js +20 -0
  19. data/lib/maximus/config/wraith/nojs.js +85 -0
  20. data/lib/maximus/config/wraith/snap.js +85 -0
  21. data/lib/maximus/constants.rb +3 -0
  22. data/lib/maximus/git_control.rb +255 -0
  23. data/lib/maximus/helper.rb +137 -0
  24. data/lib/maximus/lint.rb +201 -0
  25. data/lib/maximus/lints/brakeman.rb +61 -0
  26. data/lib/maximus/lints/jshint.rb +20 -0
  27. data/lib/maximus/lints/railsbp.rb +51 -0
  28. data/lib/maximus/lints/rubocop.rb +18 -0
  29. data/lib/maximus/lints/scsslint.rb +17 -0
  30. data/lib/maximus/rake_tasks.rb +13 -0
  31. data/lib/maximus/reporter/git-lines.sh +57 -0
  32. data/lib/maximus/reporter/jshint.js +28 -0
  33. data/lib/maximus/reporter/rubocop.rb +49 -0
  34. data/lib/maximus/statistic.rb +65 -0
  35. data/lib/maximus/statistics/phantomas.rb +32 -0
  36. data/lib/maximus/statistics/stylestats.rb +111 -0
  37. data/lib/maximus/statistics/wraith.rb +88 -0
  38. data/lib/maximus/tasks/be.rake +35 -0
  39. data/lib/maximus/tasks/fe.rake +30 -0
  40. data/lib/maximus/tasks/maximus.rake +39 -0
  41. data/lib/maximus/tasks/statistic.rake +26 -0
  42. data/lib/maximus/version.rb +3 -0
  43. data/maximus.gemspec +32 -0
  44. data/roadmap.md +17 -0
  45. metadata +243 -0
@@ -0,0 +1,137 @@
1
+ require 'rainbow'
2
+ require 'rainbow/ext/string'
3
+ require 'active_support'
4
+ require 'active_support/core_ext/object/blank'
5
+ require 'yaml'
6
+
7
+ module Maximus
8
+ module Helper
9
+
10
+ # See if Rails or a framework, i.e. Middleman
11
+ # This will usually be stored as a class variable in the inherited class, like @@is_rails = is_rails? in lint.rb
12
+ # Returns Boolean
13
+ def is_rails?
14
+ defined?(Rails)
15
+ end
16
+
17
+ # Get root directory of file being called
18
+ # Returns String (path)
19
+ def root_dir
20
+ is_rails? ? Rails.root.to_s : Dir.pwd.to_s
21
+ end
22
+
23
+ # Verify that node module is installed on the box before continuing
24
+ # Continues if module exists
25
+ def node_module_exists(node_module, install_instructions = 'npm install -g')
26
+ cmd = `if hash #{node_module} 2>/dev/null; then
27
+ echo "true"
28
+ else
29
+ echo "false"
30
+ fi`
31
+ if cmd.include? "false"
32
+ command_msg = "Missing command #{node_module}".color(:red)
33
+ abort "#{command_msg}: Please run `#{install_instructions} #{node_module}` And try again\n"
34
+ end
35
+ end
36
+
37
+ # Look for a custom config in the app's config/ directory; otherwise, use the built-in one
38
+ # TODO - best practice that this inherits the @opts from the model it's being included in?
39
+ # Returns String
40
+ def check_default(filename)
41
+ user_file = "#{@opts[:root_dir]}/config/#{filename}"
42
+ File.exist?(user_file) ? user_file : File.join(File.dirname(__FILE__), "config/#{filename}")
43
+ end
44
+
45
+ # Grab the absolute path of the reporter file
46
+ # Returns String
47
+ def reporter_path(filename)
48
+ File.join(File.dirname(__FILE__),"reporter/#{filename}")
49
+ end
50
+
51
+ # Find all files that were linted by extension
52
+ # Returns Array
53
+ def file_list(path, ext = 'scss', remover = '')
54
+ # Necessary so that directories aren't counted
55
+ collect_path = path.include?("*") ? path : "#{path}/**/*.#{ext}"
56
+ # Remove first slash from path if present. probably a better way to do this.
57
+ Dir[collect_path].collect { |file| file.gsub(remover, '').gsub(/^\/app\//, 'app/') if File.file?(file) }
58
+ end
59
+
60
+ # Count how many files were linted
61
+ # Returns Integer
62
+ def file_count(path, ext = 'scss')
63
+ file_list(path, ext).length
64
+ end
65
+
66
+ # Convert string to boolean
67
+ # Returns Boolean
68
+ def truthy(str)
69
+ return true if str == true || str =~ (/^(true|t|yes|y|1)$/i)
70
+ return false if str == false || str.blank? || str =~ (/^(false|f|no|n|0)$/i)
71
+ end
72
+
73
+ # Edit and save a YAML file
74
+ # Returns closed File
75
+ def edit_yaml(yaml_location, &block)
76
+ d = YAML.load_file(yaml_location)
77
+ block.call(d)
78
+ File.open(yaml_location, 'w') {|f| f.write d.to_yaml }
79
+ end
80
+
81
+ # Request user input
82
+ # Returns user input as String
83
+ def prompt(*args)
84
+ print(*args)
85
+ STDIN.gets
86
+ end
87
+
88
+ # Defines base log
89
+ # Returns @@log variable for use
90
+ def mlog
91
+ @@log ||= Logger.new(STDOUT)
92
+ @@log.level ||= Logger::INFO
93
+ @@log
94
+ end
95
+
96
+ # Determine if current process was called by a rake task
97
+ # Returns Boolean
98
+ # http://stackoverflow.com/questions/2467208/how-can-i-tell-if-rails-code-is-being-run-via-rake-or-script-generate
99
+ def is_rake_task?
100
+ File.basename($0) == 'rake'
101
+ end
102
+
103
+ # Convert the array from lines_added into spelled-out ranges
104
+ # Example: lines_added = {'filename' => ['0..10', '11..14']}
105
+ # Becomes {'filename' => {[0,1,2,3,4,5,6,7,8,9,10], [11,12,13,14]}}
106
+ # This is a git_control helper primarily but it's used in Lint
107
+ # TODO - I'm sure there's a better way of doing this
108
+ # TODO - figure out a better place to put this than in Helper
109
+ # Returns Hash of spelled-out arrays of integers
110
+ def lines_added_to_range(file)
111
+ changes_array = file[:changes].map { |ch| ch.split("..").map(&:to_i) }
112
+ changes_array.map { |e| (e[0]..e[1]).to_a }.flatten!
113
+ end
114
+
115
+ # Ensure @path exists
116
+ # Returns Boolean
117
+ def path_exists(path = @path)
118
+ path = path.split(' ') if path.is_a?(String) && path.include?(' ')
119
+ if path.is_a?(Array)
120
+ path.each do |p|
121
+ unless File.exist?(p)
122
+ puts "#{p} does not exist"
123
+ return false
124
+ end
125
+ end
126
+ else
127
+ if File.exist?(path)
128
+ return true
129
+ else
130
+ puts "#{path} does not exist"
131
+ return false
132
+ end
133
+ end
134
+ end
135
+
136
+ end
137
+ end
@@ -0,0 +1,201 @@
1
+ require 'json'
2
+
3
+ module Maximus
4
+ class Lint
5
+ attr_accessor :output
6
+
7
+ include Helper
8
+
9
+ # opts - Lint options (default: {})
10
+ # :is_dev
11
+ # :git_files
12
+ # :root_dir
13
+ # :path
14
+ # all lints should start with the following defined:
15
+ # def result method to handle the actual parsing
16
+ # TODO - should this be def output to be more consistent?
17
+ # Didn't want to trip over the instance variable @output
18
+ # `@task = 'name'` (__method__.to_s works fine)
19
+ # `@path ||= 'path/or/**/glob/to/files'` (string)
20
+ # they should contain the data output from the linter in JSON or
21
+ # JSON.parse format with the styles defined in README.md
22
+ # they should end with something similar to
23
+ # `@output[:files_inspected] ||= files_inspected(extension, delimiter, base_path_replacement)`
24
+ # `refine data_from_output`
25
+ def initialize(opts = {})
26
+ opts[:is_dev] ||= false
27
+ opts[:root_dir] ||= root_dir
28
+
29
+ @@log ||= mlog
30
+ @@is_rails ||= is_rails?
31
+ @@is_dev = opts[:is_dev]
32
+ @path = opts[:path]
33
+ @opts = opts
34
+ @output = {}
35
+ end
36
+
37
+ # Convert raw data into warnings, errors, conventions or refactors. Use this wisely.
38
+ # Returns complete @output as Hash
39
+ def refine(data)
40
+ # Prevent abortive empty JSON.parse error
41
+ data = '{}' if data.blank?
42
+ data = data.is_a?(String) ? JSON.parse(data) : data
43
+ @output[:relevant_lints] = relevant_lints( data, @opts[:git_files] ) unless @opts[:git_files].blank?
44
+ if @opts[:commit]
45
+ data = @output[:relevant_lints]
46
+ end
47
+ lint_warnings = []
48
+ lint_errors = []
49
+ lint_conventions = []
50
+ lint_refactors = []
51
+ unless data.blank?
52
+ data.each do |filename, error_list|
53
+ error_list.each do |message|
54
+ # so that :raw_data remains unaffected
55
+ message = message.clone
56
+ message.delete('length')
57
+ message['filename'] = filename
58
+ if message['severity'] == 'warning'
59
+ message.delete('severity')
60
+ lint_warnings << message
61
+ elsif message['severity'] == 'error'
62
+ message.delete('severity')
63
+ lint_errors << message
64
+ elsif message['severity'] == 'convention'
65
+ message.delete('severity')
66
+ lint_conventions << message
67
+ elsif message['severity'] == 'refactor'
68
+ message.delete('severity')
69
+ lint_refactors << message
70
+ end
71
+ end
72
+ end
73
+ end
74
+ @output[:lint_errors] = lint_errors
75
+ @output[:lint_warnings] = lint_warnings
76
+ @output[:lint_conventions] = lint_conventions
77
+ @output[:lint_refactors] = lint_refactors
78
+ lint_count = (lint_errors.length + lint_warnings.length + lint_conventions.length + lint_refactors.length)
79
+ if @@is_dev
80
+ lint_dev_format data unless data.blank?
81
+ puts lint_summarize
82
+ lint_ceiling lint_count
83
+ else
84
+ @@log.info lint_summarize
85
+ # Because this should be returned in the format it was received
86
+ @output[:raw_data] = data.to_json
87
+ end
88
+ @output
89
+ end
90
+
91
+
92
+ protected
93
+
94
+ # List all files inspected
95
+ # Returns Array
96
+ def files_inspected(ext, delimiter = ',', replace = @opts[:root_dir])
97
+ @path.is_a?(Array) ? @path.split(delimiter) : file_list(@path, ext, replace)
98
+ end
99
+
100
+ # Compare lint output with lines changed in commit
101
+ # Returns Array of lints that match the lines in commit
102
+ def relevant_lints(lint, files)
103
+ all_files = {}
104
+ files.each do |file|
105
+
106
+ # sometimes data will be blank but this is good - it means no errors raised in the lint
107
+ unless lint.blank?
108
+ lint_file = lint[file[:filename].to_s]
109
+
110
+ expanded = lines_added_to_range(file)
111
+ revert_name = file[:filename].gsub("#{@opts[:root_dir]}/", '')
112
+ unless lint_file.blank?
113
+ all_files[revert_name] = []
114
+
115
+ # TODO - originally I tried .map and delete_if, but this works,
116
+ # and the other method didn't cover all bases.
117
+ # Gotta be a better way to write this though
118
+ lint_file.each do |l|
119
+ if expanded.include?(l['line'].to_i)
120
+ all_files[revert_name] << l
121
+ end
122
+ end
123
+ # If there's nothing there, then it definitely isn't a relevant lint
124
+ all_files.delete(revert_name) if all_files[revert_name].blank?
125
+ end
126
+ else
127
+ # Optionally store the filename with a blank array
128
+ # all_files[file[:filename].to_s.gsub("#{@opts[:root_dir]}/", '')] = []
129
+ end
130
+ end
131
+ @output[:files_linted] = all_files.keys
132
+ all_files
133
+ end
134
+
135
+
136
+ private
137
+
138
+ # Send abbreviated results to console or to the log
139
+ # Returns console message
140
+ def lint_summarize
141
+ puts "\n" if @@is_dev
142
+
143
+ puts "#{'Warning'.color(:red)}: #{@output[:lint_errors].length} errors found in #{@task.to_s}" if @output[:lint_errors].length > 0
144
+
145
+ success = @task.to_s.color(:green)
146
+ success += ": "
147
+ success += "[#{@output[:lint_warnings].length}]".color(:yellow)
148
+ success += " " + "[#{@output[:lint_errors].length}]".color(:red)
149
+ if @task == 'rubocop'
150
+ success += " " + "[#{@output[:lint_conventions].length}]".color(:cyan)
151
+ success += " " + "[#{@output[:lint_refactors].length}]".color(:white)
152
+ end
153
+
154
+ success
155
+ end
156
+
157
+ # If there's just too much to handle, through a warning. MySQL may not store everything and throw an abortive error if the blob is too large
158
+ # Returns prompt if lint_length is greater than 100
159
+ # If prompt returns truthy, execution continues
160
+ def lint_ceiling(lint_length)
161
+ if lint_length > 100
162
+ lint_dev_format
163
+ failed_task = "#{@task}".color(:green)
164
+ errors = Rainbow("#{lint_length} failures.").red
165
+ errormsg = ["You wouldn't stand a chance in Rome.\nResolve thy errors and train with #{failed_task} again.", "The gods frown upon you, mortal.\n#{failed_task}. Again.", "Do not embarrass the city. Fight another day. Use #{failed_task}.", "You are without honor. Replenish it with another #{failed_task}.", "You will never claim the throne with a performance like that.", "Pompeii has been lost.", "A wise choice. Do not be discouraged from another #{failed_task}."].sample
166
+ errormsg += "\n\n"
167
+
168
+ go_on = prompt "\n#{errors} Continue? (y/n) "
169
+ abort errormsg unless truthy(go_on)
170
+ end
171
+ end
172
+
173
+ # Dev display, used for the rake task
174
+ # Returns console message
175
+ def lint_dev_format(errors = @output[:raw_data])
176
+ pretty_output = ''
177
+ errors.each do |filename, error_list|
178
+ pretty_output += "\n"
179
+ filename = filename.gsub("#{@opts[:root_dir]}/", '')
180
+ pretty_output += filename.color(:cyan).underline
181
+ pretty_output += "\n"
182
+ error_list.each do |message|
183
+ pretty_output += case message['severity']
184
+ when 'warning' then 'W'.color(:yellow)
185
+ when 'error' then 'E'.color(:red)
186
+ when 'convention' then 'C'.color(:cyan)
187
+ when 'refactor' then 'R'.color(:white)
188
+ else '?'.color(:blue)
189
+ end
190
+ pretty_output += ' '
191
+ pretty_output += message['line'].to_s.color(:blue)
192
+ pretty_output += " #{message['linter'].color(:green)}: "
193
+ pretty_output += message['reason']
194
+ pretty_output += "\n"
195
+ end
196
+ end
197
+ puts pretty_output
198
+ end
199
+
200
+ end
201
+ end
@@ -0,0 +1,61 @@
1
+ module Maximus
2
+ class Brakeman < Maximus::Lint
3
+
4
+ # Brakeman (requires Rails)
5
+ def result
6
+
7
+ return unless @@is_rails
8
+
9
+ @task = 'brakeman'
10
+ @path ||= @opts[:root_dir]
11
+
12
+ return unless path_exists(@path)
13
+
14
+ tmp = Tempfile.new('brakeman')
15
+ quietly { `brakeman #{@path} -f json -o #{tmp.path} -q` }
16
+ brakeman = tmp.read
17
+ tmp.close
18
+ tmp.unlink
19
+
20
+ unless brakeman.blank?
21
+ bjson = JSON.parse(brakeman)
22
+ @output[:ignored_warnings] = bjson['scan_info']['ignored_warnings']
23
+ @output[:checks_performed] = bjson['scan_info']['checks_performed']
24
+ @output[:number_of_controllers] = bjson['scan_info']['number_of_controllers']
25
+ @output[:number_of_models] = bjson['scan_info']['number_of_models']
26
+ @output[:number_of_templates] = bjson['scan_info']['number_of_templates']
27
+ @output[:ruby_version] = bjson['scan_info']['ruby_version']
28
+ @output[:rails_version] = bjson['scan_info']['rails_version']
29
+ brakeman = {}
30
+ ['warnings', 'errors'].each do |type|
31
+ new_brakeman = bjson[type].group_by { |s| s['file'] }
32
+ new_brakeman.each do |file, errors|
33
+ if file
34
+ brakeman[file.to_sym] = errors.map { |e| hash_for_brakeman(e, type) }
35
+ end
36
+ end
37
+ end
38
+ brakeman = JSON.parse(brakeman.to_json) #don't event ask
39
+ end
40
+
41
+ @output[:files_inspected] ||= files_inspected('rb', ' ')
42
+ refine brakeman
43
+ end
44
+
45
+
46
+ private
47
+
48
+ # Convert to maximus format
49
+ def hash_for_brakeman(error, type)
50
+ {
51
+ linter: error['warning_type'],
52
+ severity: type.chomp('s'),
53
+ reason: error['message'],
54
+ column: 0,
55
+ line: error['line'].to_i,
56
+ confidence: error['confidence']
57
+ }
58
+ end
59
+
60
+ end
61
+ end
@@ -0,0 +1,20 @@
1
+ module Maximus
2
+ class Jshint < Maximus::Lint
3
+
4
+ # JSHint (requires node module)
5
+ def result
6
+ @task = 'jshint'
7
+ @path ||= @@is_rails ? "#{@opts[:root_dir]}/app/assets" : "#{@opts[:root_dir]}source/assets"
8
+
9
+ return unless path_exists(@path)
10
+
11
+ node_module_exists(@task)
12
+
13
+ jshint = `jshint #{@path} --config=#{check_default('jshint.json')} --exclude-path=#{check_default('.jshintignore')} --reporter=#{reporter_path('jshint.js')}`
14
+
15
+ @output[:files_inspected] ||= files_inspected('js')
16
+ refine jshint
17
+ end
18
+
19
+ end
20
+ end
@@ -0,0 +1,51 @@
1
+ module Maximus
2
+ class Railsbp < Maximus::Lint
3
+
4
+ # rails_best_practice (requires Rails)
5
+ def result
6
+
7
+ return unless @@is_rails
8
+
9
+ @task = 'railsbp'
10
+ @path ||= @opts[:root_dir]
11
+
12
+ return unless path_exists(@path)
13
+
14
+ tmp = Tempfile.new('railsbp')
15
+ `rails_best_practices #{@path} -f json --output-file #{tmp.path}`
16
+ railsbp = tmp.read
17
+ tmp.close
18
+ tmp.unlink
19
+
20
+ unless railsbp.blank?
21
+ rbj = JSON.parse(railsbp).group_by { |s| s['filename'] }
22
+ railsbp = {}
23
+ rbj.each do |file, errors|
24
+ if file
25
+ # This crazy gsub grapbs scrubs the absolute path from the filename
26
+ railsbp[file.gsub(Rails.root.to_s, '')[1..-1].to_sym] = errors.map { |o| hash_for_railsbp(o) }
27
+ end
28
+ end
29
+ railsbp = JSON.parse(railsbp.to_json) #don't event ask
30
+ end
31
+
32
+ @output[:files_inspected] ||= files_inspected('rb', ' ')
33
+ refine railsbp
34
+ end
35
+
36
+
37
+ private
38
+
39
+ # Convert to maximus format
40
+ def hash_for_railsbp(error)
41
+ {
42
+ linter: error['message'].gsub(/\((.*)\)/, '').strip.parameterize('_').camelize,
43
+ severity: 'warning',
44
+ reason: error['message'],
45
+ column: 0,
46
+ line: error['line_number'].to_i
47
+ }
48
+ end
49
+
50
+ end
51
+ end