dudity 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.
@@ -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: []