dudity 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 2f4f733d0f1d5e557bf7a80436baa2a66a3fe8d689a313f0eed01b728ac3d65d
4
+ data.tar.gz: 945da3f26fcaa3656824de52f471731fe091d5fe7c991ff81d45af46cfb0b8db
5
+ SHA512:
6
+ metadata.gz: 272d70947283eb90ebcd0248d7a2813b5fcb26b88e750d735c788cb6176ca41acf9cdd209dfc732dc3e3d723b2f2d7dd9f3eba5bbe54dea90033917f9b2b83cb
7
+ data.tar.gz: 33a6432c3575142ecea4d6de618748f3c94762b838e105e0037ed6a775138f6d0ac2707c35f2fc04ce795ec1eb9a72306ca3b23c5714de5da1750f3020d074c6
@@ -0,0 +1,10 @@
1
+ require 'open-uri'
2
+
3
+ class DownloadService
4
+ def self.call(url, mode = :default)
5
+ begin
6
+ mode == :read_by_line ? open(url).readlines : open(url).read
7
+ rescue OpenURI::HTTPError
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,82 @@
1
+ # this module was created by Victor Shepelev (zverok) https://github.com/zverok
2
+ require 'fast'
3
+
4
+ module Dudes
5
+ class Calculator
6
+ using(Module.new do
7
+ refine Astrolabe::Node do
8
+ def fast(path)
9
+ Fast.search(path, self)
10
+ end
11
+
12
+ # Lot of chunks of code parsed as (:foo, ...) if it is a single statement,
13
+ # or (:begin, (:foo, ...), (:bar, ...)) if it is several independent statements.
14
+ # Robustly make it an array of nodes
15
+ def arrayify
16
+ type == :begin ? children : [self]
17
+ end
18
+ end
19
+ end)
20
+
21
+ def initialize(code)
22
+ @ast = Fast.ast(code)
23
+ end
24
+
25
+ def call
26
+ return [] unless @ast
27
+ @ast.arrayify.map(&method(:calc_class)).compact
28
+ end
29
+
30
+ private
31
+
32
+ def calc_class(node)
33
+ return unless node.type == :class
34
+
35
+ name, parent, body = *node
36
+ {
37
+ name: name.children.compact.join(':'),
38
+ methods: body&.arrayify&.map(&method(:calc_method))&.compact || [],
39
+ references: extract_references(body)
40
+ }
41
+ end
42
+
43
+ def extract_references(node)
44
+ return [] unless node
45
+
46
+ node.fast('(const {nil _} )').map { |n| n.children.compact.join('::') }.sort.uniq
47
+ end
48
+
49
+ def calc_method(node)
50
+ return unless node.type == :def
51
+
52
+ name, args, body = *node
53
+
54
+ return empty_body(name, args) if body.nil?
55
+
56
+ {
57
+ name: name,
58
+ args: args.children.count,
59
+ length: count_statements(body.arrayify),
60
+ conditions: count_conditions(body),
61
+ }
62
+ end
63
+
64
+ def empty_body(name, args)
65
+ {
66
+ name: name,
67
+ args: args.children.count,
68
+ length: 0,
69
+ conditions: 0,
70
+ }
71
+ end
72
+
73
+ # FIXME: This is kinda naive... But maybe appropriate enough
74
+ def count_statements(nodes)
75
+ nodes.sum { |n| n.each_node.count }
76
+ end
77
+
78
+ def count_conditions(node)
79
+ node.fast('({if case} _)').count
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,155 @@
1
+ require 'dudegl'
2
+ require 'open-uri'
3
+
4
+ Dir[File.dirname(__FILE__) + '/**/*.rb'].each {|file| require_relative file }
5
+
6
+ class Dudity
7
+ class << self
8
+ def visualise(path, opt = {})
9
+ @path = path
10
+ opt.key?(:except) ? except = opt[:except] : except = nil
11
+ opt.key?(:only) ? only = opt[:only] : only = nil
12
+ opt.key?(:ignore_classes) ? ignore_classes = opt[:ignore_classes] : ignore_classes = nil
13
+ opt.key?(:only_classes) ? only_classes = opt[:only_classes] : only_classes = nil
14
+ opt.key?(:filename_suffix) ? filename_suffix = opt[:filename_suffix] : filename_suffix = ''
15
+
16
+ project_files = ScanApp.call(@path, except, only)
17
+ app_name = @path.split('/').last
18
+
19
+ @params_list = []
20
+ project_files.each { |project_file| process_item(project_file) }
21
+
22
+ @params_list = @params_list.flatten.compact
23
+ exclude_classes(ignore_classes) if ignore_classes
24
+ include_classes(only_classes) if only_classes
25
+
26
+ dudes = DudeGl.new @params_list, dudes_per_row_max: 4
27
+ dudes.render
28
+ dudes.save "#{app_name}#{filename_suffix}"
29
+ end
30
+
31
+ def visualise_diff(path_to_diff, opt = {})
32
+ @params1 = []
33
+ @params2 = []
34
+
35
+ # path to the dir where diff file is stored
36
+ @path = path_to_diff.split('/').take(path_to_diff.split('/').size - 1).join('/')
37
+
38
+ opt.key?(:as) ? file_type = opt[:as] : file_type = :svg
39
+ opt.key?(:pull_branch) ? @pull_branch = opt[:pull_branch] : return
40
+
41
+ diff = open(path_to_diff).readlines
42
+ @diff_data = GitDiffService.call(diff)
43
+
44
+ return generate_svg if file_type == :svg
45
+ return generate_html_report if file_type == :html
46
+ end
47
+
48
+ def visualise_pr(public_pr_link, opt = {})
49
+ @params1 = []
50
+ @params2 = []
51
+
52
+ @path = public_pr_link
53
+ diff_url = "#{public_pr_link}.diff"
54
+
55
+ opt.key?(:as) ? file_type = opt[:as] : file_type = :svg
56
+ opt.key?(:pull_branch) ? @pull_branch = opt[:pull_branch] : return
57
+
58
+ diff = DownloadService.call(diff_url, :read_by_line)
59
+ @diff_data = GitDiffService.call(diff)
60
+
61
+ return generate_svg if file_type == :svg
62
+ return generate_html_report if file_type == :html
63
+ end
64
+
65
+ private
66
+
67
+ # generate name based on pull request data. Example: DudesHub_pull_5
68
+ def fname
69
+ @path.split('/')[-3, 3].join('_')
70
+ end
71
+
72
+ # generate html report title based on repo data, make each word capitalized
73
+ def report_title
74
+ @path.split('/')[-3, 3].map(&:capitalize).join(' ')
75
+ end
76
+
77
+ def generate_svg
78
+ analyze_code(@diff_data)
79
+ end
80
+
81
+ def generate_html_report
82
+ separate_code
83
+ analyze_by_category
84
+
85
+ html = open('templates/dudes_report.html').read
86
+ html = html.sub('[dudes_report_title]', report_title)
87
+ @report_file_path = "#{fname}.html"
88
+ File.open(@report_file_path, 'w') { |file| file.write(html) }
89
+ end
90
+
91
+ def separate_code
92
+ @diff_data_controllers = []
93
+ @diff_data_models = []
94
+ @diff_data_others = []
95
+
96
+ @diff_data.each do |item|
97
+ if item[:old_name]&.start_with?("app/controllers") || item[:new_name]&.start_with?("app/controllers")
98
+ @diff_data_controllers << item
99
+ elsif item[:old_name]&.start_with?("app/models") || item[:new_name]&.start_with?("app/models")
100
+ @diff_data_models << item
101
+ else
102
+ @diff_data_others << item
103
+ end
104
+ end
105
+ end
106
+
107
+ def analyze_by_category
108
+ analyze_code(@diff_data_controllers, :controllers) if !@diff_data_controllers.empty?
109
+ analyze_code(@diff_data_models, :models) if !@diff_data_models.empty?
110
+ analyze_code(@diff_data_others, :others) if !@diff_data_others.empty?
111
+ end
112
+
113
+ def analyze_code(diff_data, label = nil)
114
+ @params1 = []
115
+ @params2 = []
116
+ local = !@path.start_with?('http')
117
+ local ? suffix = '_local' : suffix = ''
118
+
119
+ diff_data.map { |item| process_item_for_diff(item, local) }
120
+
121
+ renamed = diff_data.select { |item| item[:status] == :renamed_class }
122
+
123
+ return false if params_empty?
124
+
125
+ dudes = DudeGl.new [@params1.flatten.compact, @params2.flatten.compact],
126
+ dudes_per_row_max: 4, renamed: renamed, diff: true
127
+ dudes.render
128
+
129
+ label ? dudes.save("#{label}#{suffix}") : dudes.save("#{fname}#{suffix}")
130
+ end
131
+
132
+ def process_item(project_file)
133
+ code = open(project_file).read
134
+ @params_list << Dudes::Calculator.new(code).call
135
+ end
136
+
137
+ def process_item_for_diff(item, local = false)
138
+ processed_code = ProcessCodeService.new(@path, @pull_branch, item, local = local).call
139
+ @params1 << processed_code.first
140
+ @params2 << processed_code.last
141
+ end
142
+
143
+ def params_empty?
144
+ @params1.empty? || @params2.empty?
145
+ end
146
+
147
+ def exclude_classes(classes)
148
+ @params_list = @params_list.reject { |param| classes.any? { |item| param[:name] == item } }
149
+ end
150
+
151
+ def include_classes(classes)
152
+ @params_list = @params_list.select { |param| classes.any? { |item| param[:name] == item } }
153
+ end
154
+ end
155
+ end
@@ -0,0 +1,119 @@
1
+ class GitDiffService
2
+ class << self
3
+ PATTERN_MATCH = /^diff --git\s/
4
+ # restrict scope: for Rails app exclude autogenerated code and omit any files except .rb
5
+ EXCLUDE_ROOT_FOLDERS = ['db/', 'spec/', 'config/']
6
+ ALLOWED_EXTENSION = '.rb'
7
+
8
+ def call(diff_lines, exclude_folders = [])
9
+ @diff_lines = diff_lines
10
+ # TO-DO: support @only_folders
11
+ @exclude_folders = exclude_folders + EXCLUDE_ROOT_FOLDERS
12
+
13
+ patch_pos_start = lines_with_file_paths.map { |line| patch_start_pos(line) }
14
+ # array with start and end positions for each patch
15
+ @patches_pos = patch_pos_start.zip(patch_end_pos(patch_pos_start))
16
+ slice_diff
17
+ @patches = @patches.select { |patch| allowed_patch?(patch) }
18
+ @diff_data = @patches.map { |patch| fname_diff(patch) }
19
+ @diff_data += @patches.map { |patch| class_diff(patch) }
20
+ @diff_data.compact
21
+ end
22
+
23
+ private
24
+
25
+ # split diff lines into separate patches for each file
26
+ def slice_diff
27
+ @patches = @patches_pos.map { |pos| @diff_lines[pos.first..pos.last] }
28
+ end
29
+
30
+ def allowed_patch?(patch)
31
+ fname_string = patch.first
32
+ extension_allowed = extract_file_paths(fname_string).last.end_with?(ALLOWED_EXTENSION)
33
+ folder_allowed = !extract_file_paths(fname_string).last.start_with?(*@exclude_folders)
34
+
35
+ extension_allowed && folder_allowed
36
+ end
37
+
38
+ def new_file?(patch)
39
+ patch.select { |line| line.start_with?('new file mode ') }.any?
40
+ end
41
+
42
+ def deleted_file?(patch)
43
+ patch.select { |line| line.start_with?('deleted file mode ') }.any?
44
+ end
45
+
46
+ def renamed_file?(patch)
47
+ patch.select { |line| line.start_with?('rename from ') }.any?
48
+ end
49
+
50
+ def class_renamed?(patch)
51
+ patch.select { |line| line.start_with?('-class') }.any? &&
52
+ patch.select { |line| line.start_with?('+class') }.any?
53
+ end
54
+
55
+ def old_class_name(patch)
56
+ res = patch.select { |line| line.start_with?('-class') }
57
+ return if res.empty?
58
+ line = res.first
59
+ line.split(' ').last.strip
60
+ end
61
+
62
+ def new_class_name(patch)
63
+ res = patch.select { |line| line.start_with?('+class') }
64
+ return if res.empty?
65
+ line = res.first
66
+ line.split(' ').last.strip
67
+ end
68
+
69
+ def fname_diff(patch)
70
+ # first line in patch contains info about file path
71
+ file_path_line = patch.first
72
+ return { old_name: extract_file_paths(file_path_line).first,
73
+ new_name: extract_file_paths(file_path_line).last, status: :renamed } if renamed_file?(patch)
74
+ return { old_name: nil, new_name: extract_file_paths(file_path_line).last,
75
+ status: :new } if new_file?(patch)
76
+ return { old_name: extract_file_paths(file_path_line).first,
77
+ new_name: nil, status: :deleted } if deleted_file?(patch)
78
+ # changed file without renaming
79
+ return { old_name: extract_file_paths(file_path_line).first,
80
+ new_name: extract_file_paths(file_path_line).last, status: :changed }
81
+ end
82
+
83
+ def class_diff(patch)
84
+ return { old_name: old_class_name(patch),
85
+ new_name: new_class_name(patch), status: :renamed_class } if class_renamed?(patch)
86
+ end
87
+
88
+ # end line index for current patch
89
+ def patch_end_pos(patch_pos_start)
90
+ end_line_pos = patch_pos_start.size - 1
91
+ pos_end = patch_pos_start.map.with_index do |pos, i|
92
+ i == patch_pos_start.size - 1 ? @diff_lines.size - 1 : patch_pos_start[i + 1] - 1
93
+ end
94
+
95
+ pos_end
96
+ end
97
+
98
+ def lines_with_file_paths
99
+ @diff_lines.select { |line| line.scan(PATTERN_MATCH).any? }
100
+ end
101
+
102
+ def patch_start_pos(line)
103
+ @diff_lines.index(line)
104
+ end
105
+
106
+ def extract_file_paths(line)
107
+ # diff --git a/project_v1/README.md b/project_v2/README2.md
108
+ line = line.delete_prefix('diff --git ')
109
+ path1, path2 = line.split(' ')
110
+ [path1, path2].map! { |path| file_path(path) }
111
+ end
112
+
113
+ def file_path(str)
114
+ # a/project_v1/README.md -> project_v1/README.md
115
+ prefix, *path_parts = str.split('/')
116
+ path_parts.join('/')
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,87 @@
1
+ # use source code of gems to simplify their code editing (if improvements are needed or bugs are found)
2
+ # in the final version of app gems will be used
3
+ require '/Users/dmkp/Documents/code/ruby/dudes/zverok_dudes_fork2/dudes/lib/dudes.rb'
4
+
5
+ class ProcessCodeService
6
+ # path is link to pr or treated as local path if local = true
7
+ def initialize(path, pull_branch, file_data, local = false)
8
+ @path = path
9
+ @pull_branch = pull_branch
10
+ @file_data = file_data
11
+ @local = local
12
+ end
13
+
14
+ def call
15
+ process_code_file
16
+
17
+ [@params1, @params2]
18
+ end
19
+
20
+ private
21
+
22
+ def process_code_file
23
+ download_master_code
24
+ download_pull_request_code
25
+ end
26
+
27
+ def download_master_code
28
+ return unless [:deleted, :changed, :renamed].include?(@file_data[:status])
29
+ code_master = get_code(code_master_branch_path)
30
+ # code_master = DownloadService.call(code_master_branch_path)
31
+ code_hash = Dudes::Calculator.new(code_master).call
32
+ @params1 = code_hash.first unless code_hash.empty?
33
+ end
34
+
35
+ def download_pull_request_code
36
+ return unless [:new, :changed, :renamed].include?(@file_data[:status])
37
+ code_pull_request = get_code(code_pull_request_branch_path)
38
+ # code_pull_request = DownloadService.call(code_pull_request_branch_path)
39
+ code_hash = Dudes::Calculator.new(code_pull_request).call
40
+ @params2 = code_hash.first unless code_hash.empty?
41
+ end
42
+
43
+ def get_code(path)
44
+ return open(path).read if @local
45
+ return DownloadService.call(path)
46
+ end
47
+
48
+ def code_master_branch_path
49
+ code_path = extract_code_path(:master)
50
+ return "#{@path}/#{repo_full_name}-master/#{code_path}" if @local
51
+ return "https://raw.githubusercontent.com/#{repo_full_name}/master/#{code_path}"
52
+ end
53
+
54
+ def code_pull_request_branch_path
55
+ code_path = extract_code_path(:pull)
56
+ return "#{@path}/#{repo_full_name}-#{@pull_branch}/#{code_path}" if @local
57
+ return "https://raw.githubusercontent.com/#{repo_full_name}/#{@pull_branch}/#{code_path}"
58
+ end
59
+
60
+ def repo_full_name
61
+ return @path.split('/').last if @local
62
+ return @path.split('/')[-4, 2].join('/')
63
+ end
64
+
65
+ def extract_code_path(branch)
66
+ return @file_data[:old_name] if deleted?
67
+ return @file_data[:new_name] if new? || changed?
68
+ return renamed?(branch)
69
+ end
70
+
71
+ def deleted?
72
+ @file_data[:status] == :deleted
73
+ end
74
+
75
+ def new?
76
+ @file_data[:status] == :new
77
+ end
78
+
79
+ def changed?
80
+ @file_data[:status] == :changed
81
+ end
82
+
83
+ def renamed?(branch)
84
+ return @file_data[:old_name] if branch == :master
85
+ return @file_data[:new_name] if branch == :pull
86
+ end
87
+ end
@@ -0,0 +1,42 @@
1
+ class ScanApp
2
+ class << self
3
+ EXCLUDE_FOLDERS = ['db/', 'spec/', 'config/']
4
+ ALLOWED_EXTENSIONS = ['.rb']
5
+
6
+ def call(path, except, only)
7
+ @except = except
8
+ @only = only
9
+ # list of all files from app dir recursively
10
+ @project_files = Dir.glob("#{path}/**/*")
11
+
12
+ filter_by_extension
13
+ exclude_default
14
+
15
+ exclude_files if @except
16
+ include_only_files if @only
17
+ @project_files
18
+ end
19
+
20
+ private
21
+
22
+ def exclude_default
23
+ @project_files = @project_files.reject { |project_file| match?(project_file, EXCLUDE_FOLDERS) }
24
+ end
25
+
26
+ def filter_by_extension
27
+ @project_files = @project_files.select { |project_file| project_file.end_with?(*ALLOWED_EXTENSIONS) }
28
+ end
29
+
30
+ def exclude_files
31
+ @project_files = @project_files.reject { |project_file| match?(project_file, @except) }
32
+ end
33
+
34
+ def include_only_files
35
+ @project_files = @project_files.select { |project_file| match?(project_file, @only) }
36
+ end
37
+
38
+ def match?(string, matches)
39
+ matches.any? { |item| string.include?(item) }
40
+ end
41
+ end
42
+ end
metadata ADDED
@@ -0,0 +1,49 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: dudity
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Dmitry Khramtsov
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2020-03-11 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: Try DudeGL code visualization in your Rails projects
14
+ email:
15
+ - dp@khramtsov.net
16
+ executables: []
17
+ extensions: []
18
+ extra_rdoc_files: []
19
+ files:
20
+ - lib/download_service.rb
21
+ - lib/dudes.rb
22
+ - lib/dudity.rb
23
+ - lib/git_diff_service.rb
24
+ - lib/process_code_service.rb
25
+ - lib/scan_app.rb
26
+ homepage: https://github.com/dmikhr/Dudity
27
+ licenses:
28
+ - MIT
29
+ metadata: {}
30
+ post_install_message:
31
+ rdoc_options: []
32
+ require_paths:
33
+ - lib
34
+ required_ruby_version: !ruby/object:Gem::Requirement
35
+ requirements:
36
+ - - ">="
37
+ - !ruby/object:Gem::Version
38
+ version: '0'
39
+ required_rubygems_version: !ruby/object:Gem::Requirement
40
+ requirements:
41
+ - - ">="
42
+ - !ruby/object:Gem::Version
43
+ version: '0'
44
+ requirements: []
45
+ rubygems_version: 3.0.3
46
+ signing_key:
47
+ specification_version: 4
48
+ summary: Analyze Rails code with stick dudes
49
+ test_files: []