pmdtester 1.0.0 → 1.2.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 (67) hide show
  1. checksums.yaml +4 -4
  2. data/.ci/build.sh +99 -0
  3. data/.ci/inc/fetch_ci_scripts.bash +19 -0
  4. data/.ci/manual-integration-tests.sh +37 -0
  5. data/.github/workflows/build.yml +55 -0
  6. data/.github/workflows/manual-integration-tests.yml +43 -0
  7. data/.gitignore +9 -0
  8. data/.hoerc +1 -1
  9. data/.rubocop.yml +9 -2
  10. data/.ruby-version +1 -0
  11. data/History.md +79 -0
  12. data/Manifest.txt +28 -9
  13. data/README.rdoc +59 -33
  14. data/Rakefile +7 -5
  15. data/config/all-java.xml +1 -1
  16. data/config/design.xml +1 -1
  17. data/config/project-list.xml +8 -7
  18. data/config/projectlist_1_0_0.xsd +2 -1
  19. data/config/projectlist_1_1_0.xsd +31 -0
  20. data/config/projectlist_1_2_0.xsd +39 -0
  21. data/lib/pmdtester.rb +8 -7
  22. data/lib/pmdtester/builders/liquid_renderer.rb +130 -0
  23. data/lib/pmdtester/builders/pmd_report_builder.rb +107 -79
  24. data/lib/pmdtester/builders/project_builder.rb +105 -0
  25. data/lib/pmdtester/builders/project_hasher.rb +128 -0
  26. data/lib/pmdtester/builders/rule_set_builder.rb +96 -47
  27. data/lib/pmdtester/builders/simple_progress_logger.rb +4 -4
  28. data/lib/pmdtester/builders/summary_report_builder.rb +63 -131
  29. data/lib/pmdtester/collection_by_file.rb +55 -0
  30. data/lib/pmdtester/parsers/options.rb +24 -0
  31. data/lib/pmdtester/parsers/pmd_report_document.rb +72 -28
  32. data/lib/pmdtester/parsers/projects_parser.rb +2 -4
  33. data/lib/pmdtester/pmd_branch_detail.rb +35 -19
  34. data/lib/pmdtester/pmd_configerror.rb +23 -24
  35. data/lib/pmdtester/pmd_error.rb +34 -34
  36. data/lib/pmdtester/pmd_report_detail.rb +10 -13
  37. data/lib/pmdtester/pmd_tester_utils.rb +58 -0
  38. data/lib/pmdtester/pmd_violation.rb +66 -28
  39. data/lib/pmdtester/project.rb +42 -56
  40. data/lib/pmdtester/report_diff.rb +203 -109
  41. data/lib/pmdtester/resource_locator.rb +4 -0
  42. data/lib/pmdtester/runner.rb +67 -64
  43. data/pmdtester.gemspec +28 -37
  44. data/resources/_includes/diff_pill_row.html +6 -0
  45. data/resources/css/bootstrap.min.css +7 -0
  46. data/resources/css/datatables.min.css +36 -0
  47. data/resources/css/pmd-tester.css +132 -0
  48. data/resources/js/bootstrap.min.js +7 -0
  49. data/resources/js/code-snippets.js +73 -0
  50. data/resources/js/datatables.min.js +726 -0
  51. data/resources/js/jquery-3.2.1.slim.min.js +4 -0
  52. data/resources/js/jquery.min.js +2 -0
  53. data/resources/js/popper.min.js +5 -0
  54. data/resources/js/project-report.js +137 -0
  55. data/resources/project_diff_report.html +214 -0
  56. data/resources/project_index.html +113 -0
  57. data/resources/project_pmd_report.html +186 -0
  58. metadata +73 -25
  59. data/.travis.yml +0 -40
  60. data/lib/pmdtester/builders/diff_builder.rb +0 -31
  61. data/lib/pmdtester/builders/diff_report/configerrors.rb +0 -50
  62. data/lib/pmdtester/builders/diff_report/errors.rb +0 -71
  63. data/lib/pmdtester/builders/diff_report/violations.rb +0 -77
  64. data/lib/pmdtester/builders/diff_report_builder.rb +0 -99
  65. data/lib/pmdtester/builders/html_report_builder.rb +0 -56
  66. data/resources/css/maven-base.css +0 -155
  67. data/resources/css/maven-theme.css +0 -171
@@ -7,12 +7,12 @@ module PmdTester
7
7
  class PmdReportDetail
8
8
  attr_accessor :execution_time
9
9
  attr_accessor :timestamp
10
- attr_reader :working_dir
10
+ attr_accessor :working_dir
11
11
 
12
- def initialize
13
- @execution_time = 0
14
- @timestamp = ''
15
- @working_dir = Dir.getwd
12
+ def initialize(execution_time: 0, timestamp: '', working_dir: Dir.getwd)
13
+ @execution_time = execution_time
14
+ @timestamp = timestamp
15
+ @working_dir = working_dir
16
16
  end
17
17
 
18
18
  def save(report_info_path)
@@ -22,16 +22,13 @@ module PmdTester
22
22
  file.close
23
23
  end
24
24
 
25
- def load(report_info_path)
25
+ def self.load(report_info_path)
26
26
  if File.exist?(report_info_path)
27
- hash = JSON.parse(File.read(report_info_path))
28
- @execution_time = hash['execution_time']
29
- @timestamp = hash['timestamp']
30
- @working_dir = hash['working_dir']
31
- hash
27
+ hash = JSON.parse(File.read(report_info_path), symbolize_names: true)
28
+ PmdReportDetail.new(**hash)
32
29
  else
33
- puts "#{report_info_path} doesn't exist"
34
- {}
30
+ PmdTester.logger.warn("#{report_info_path} doesn't exist")
31
+ PmdReportDetail.new
35
32
  end
36
33
  end
37
34
 
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PmdTester
4
+ # Some functions that that don't belong in a specific class,
5
+ module PmdTesterUtils
6
+ include PmdTester
7
+
8
+ # Parse the base and the patch report, compute their diff
9
+ # Returns a +ReportDiff+
10
+ def build_report_diff(base_report_file, patch_report_file, base_info, patch_info, filter_set = nil)
11
+ base_details = PmdReportDetail.load(base_info)
12
+ patch_details = PmdReportDetail.load(patch_info)
13
+
14
+ base_report = parse_pmd_report(base_report_file, BASE, base_details, filter_set)
15
+ patch_report = parse_pmd_report(patch_report_file, PATCH, patch_details)
16
+
17
+ logger.info 'Calculating diffs'
18
+ ReportDiff.new(base_report: base_report, patch_report: patch_report)
19
+ end
20
+
21
+ # Parse the +report_file+ to produce a +Report+.
22
+ # For the schema of xml reports, refer to http://pmd.sourceforge.net/report_2_0_0.xsd
23
+ def parse_pmd_report(report_file, branch, report_details, filter_set = nil)
24
+ require 'nokogiri'
25
+
26
+ logger.info "Parsing #{report_file}"
27
+ doc = PmdReportDocument.new(branch, report_details.working_dir, filter_set)
28
+ parser = Nokogiri::XML::SAX::Parser.new(doc)
29
+ parser.parse_file(report_file) if File.exist?(report_file)
30
+ Report.new(
31
+ report_document: doc,
32
+ file: report_file,
33
+
34
+ timestamp: report_details.timestamp,
35
+ exec_time: report_details.execution_time
36
+ )
37
+ end
38
+
39
+ # Fill the report_diff field of every project
40
+ def compute_project_diffs(projects, base_branch, patch_branch, filter_set = nil)
41
+ projects.each do |project|
42
+ logger.info "Preparing report for #{project.name}"
43
+ logger.info " with filter #{filter_set}" unless filter_set.nil?
44
+ project.compute_report_diff(base_branch, patch_branch, filter_set)
45
+ end
46
+ end
47
+
48
+ # Build the diff reports and write them all
49
+ def build_html_reports(projects, base_branch_details, patch_branch_details, filter_set = nil)
50
+ compute_project_diffs(projects, base_branch_details.branch_name, patch_branch_details.branch_name,
51
+ filter_set)
52
+
53
+ SummaryReportBuilder.new.write_all_projects(projects,
54
+ base_branch_details,
55
+ patch_branch_details)
56
+ end
57
+ end
58
+ end
@@ -1,25 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PmdTester
4
- # This class is used to store pmd violations and its size.
5
- class PmdViolations
6
- attr_reader :violations
7
- attr_reader :violations_size
8
-
9
- def initialize
10
- # key:filename as String => value:PmdViolation Array
11
- @violations = {}
12
- @violations_size = 0
13
- end
14
-
15
- def add_violations_by_filename(filename, violations)
16
- return if violations.empty?
17
-
18
- @violations.store(filename, violations)
19
- @violations_size += violations.size
20
- end
21
- end
22
-
23
4
  # This class represents a 'violation' element of Pmd xml report
24
5
  # and which pmd branch the 'violation' is from
25
6
  class PmdViolation
@@ -46,23 +27,80 @@ module PmdTester
46
27
  # </xs:simpleContent>
47
28
  # </xs:complexType>
48
29
 
49
- attr_reader :attrs
50
- attr_accessor :text
30
+ attr_reader :fname, :info_url, :line, :old_line, :old_message, :rule_name, :ruleset_name
31
+ attr_accessor :message
51
32
 
52
- def initialize(attrs, branch)
53
- @attrs = attrs
33
+ # rubocop:disable Metrics/ParameterLists
34
+ # Disable it: how is replacing a long parameter list with a single hash helping?
35
+ def initialize(branch:, fname:, info_url:, bline:, rule_name:, ruleset_name:)
54
36
  @branch = branch
55
- @text = ''
37
+ @fname = fname
38
+ @message = ''
39
+
40
+ @info_url = info_url
41
+ @line = bline
42
+ @rule_name = rule_name
43
+
44
+ @ruleset_name = ruleset_name
45
+
46
+ @changed = false
47
+ @old_message = nil
48
+ @old_line = nil
49
+ end
50
+ # rubocop:enable Metrics/ParameterLists
51
+
52
+ def line_move?(other)
53
+ message.eql?(other.message) && (line - other.line).abs <= 5
54
+ end
55
+
56
+ def try_merge?(other)
57
+ if branch != BASE && branch != other.branch && rule_name == other.rule_name &&
58
+ !changed? && # not already changed
59
+ (line == other.line || line_move?(other))
60
+ @changed = true
61
+ @old_message = other.message
62
+ @old_line = other.line
63
+ true
64
+ else
65
+ false
66
+ end
67
+ end
68
+
69
+ # only makes sense if this is a diff
70
+ def added?
71
+ branch != BASE && !changed?
72
+ end
73
+
74
+ # only makes sense if this is a diff
75
+ def changed?
76
+ @changed
77
+ end
78
+
79
+ # only makes sense if this is a diff
80
+ def removed?
81
+ branch == BASE
82
+ end
83
+
84
+ def sort_key
85
+ line
56
86
  end
57
87
 
58
88
  def eql?(other)
59
- @attrs['beginline'].eql?(other.attrs['beginline']) &&
60
- @attrs['rule'].eql?(other.attrs['rule']) &&
61
- @text.eql?(other.text)
89
+ rule_name.eql?(other.rule_name) &&
90
+ line.eql?(other.line) &&
91
+ fname.eql?(other.fname) &&
92
+ message.eql?(other.message)
62
93
  end
63
94
 
64
95
  def hash
65
- [@attrs['beginline'], @attrs['rule'], @text].hash
96
+ [line, rule_name, message].hash
97
+ end
98
+
99
+ def to_liquid
100
+ {
101
+ 'branch' => branch,
102
+ 'changed' => changed?
103
+ }
66
104
  end
67
105
  end
68
106
  end
@@ -1,8 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'pathname'
4
+
3
5
  module PmdTester
4
6
  # This class represents all the information about the project
5
7
  class Project
8
+ include PmdTesterUtils
9
+
6
10
  REPOSITORIES_PATH = 'target/repositories'
7
11
 
8
12
  attr_reader :name
@@ -10,29 +14,36 @@ module PmdTester
10
14
  attr_reader :connection
11
15
  attr_reader :webview_url
12
16
  attr_reader :tag
13
- attr_reader :exclude_pattern
17
+ attr_reader :exclude_patterns
18
+ attr_reader :src_subpath
14
19
  attr_accessor :report_diff
15
20
  # key: pmd branch name as String => value: local path of pmd report
21
+ attr_reader :build_command
22
+ attr_reader :auxclasspath_command
23
+ # stores the auxclasspath calculated after cloning/preparing the project
24
+ attr_accessor :auxclasspath
16
25
 
17
- def initialize(project)
26
+ def initialize(project) # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
18
27
  @name = project.at_xpath('name').text
19
28
  @type = project.at_xpath('type').text
20
29
  @connection = project.at_xpath('connection').text
21
30
 
22
- @tag = 'master'
23
- tag_element = project.at_xpath('tag')
24
- @tag = tag_element.text unless tag_element.nil?
31
+ @tag = project.at_xpath('tag')&.text || 'master'
25
32
 
26
33
  webview_url_element = project.at_xpath('webview-url')
27
34
  @webview_url = default_webview_url
28
35
  @webview_url = webview_url_element.text unless webview_url_element.nil?
29
36
 
30
- @exclude_pattern = []
37
+ @src_subpath = project.at_xpath('src-subpath')&.text || '.'
38
+ @exclude_patterns = []
31
39
  project.xpath('exclude-pattern').each do |ep|
32
- @exclude_pattern.push(ep.text)
40
+ @exclude_patterns.push(ep.text)
33
41
  end
34
42
 
35
- @report_diff = ReportDiff.new
43
+ @build_command = project.at_xpath('build-command')&.text
44
+ @auxclasspath_command = project.at_xpath('auxclasspath-command')&.text
45
+
46
+ @report_diff = nil
36
47
  end
37
48
 
38
49
  # Generate the default webview url for the projects
@@ -49,13 +60,17 @@ module PmdTester
49
60
  # Change the file path from 'LOCAL_DIR/SOURCE_CODE_PATH' to
50
61
  # 'WEB_VIEW_URL/SOURCE_CODE_PATH'
51
62
  def get_webview_url(file_path)
52
- file_path.gsub(%r{/#{local_source_path}}, @webview_url)
63
+ file_path.gsub(%r{/#{clone_root_path}}, @webview_url)
53
64
  end
54
65
 
55
66
  # Change the file path from 'LOCAL_DIR/SOURCE_CODE_PATH' to
56
67
  # 'PROJECT_NAME/SOURCE_CODE_PATH'
57
68
  def get_path_inside_project(file_path)
58
- file_path.gsub(%r{/#{local_source_path}}, @name)
69
+ file_path.gsub(%r{/#{clone_root_path}}, @name)
70
+ end
71
+
72
+ def get_local_path(file_path)
73
+ file_path.sub(%r{/#{clone_root_path}/}, '')
59
74
  end
60
75
 
61
76
  def get_pmd_report_path(branch_name)
@@ -82,61 +97,32 @@ module PmdTester
82
97
  end
83
98
  end
84
99
 
85
- def get_project_target_dir(branch_name)
86
- branch_filename = PmdBranchDetail.branch_filename(branch_name)
87
- dir = "target/reports/#{branch_filename}/#{@name}"
88
- FileUtils.mkdir_p(dir) unless File.directory?(dir)
89
- dir
100
+ ##
101
+ # Path to the sources to analyze (below or equal to clone_root_path)
102
+ def local_source_path
103
+ # normalize path
104
+ Pathname.new("#{clone_root_path}/#{src_subpath}").cleanpath
90
105
  end
91
106
 
92
- def local_source_path
107
+ ##
108
+ # Path to the clone directory
109
+ def clone_root_path
93
110
  "#{REPOSITORIES_PATH}/#{@name}"
94
111
  end
95
112
 
96
- def target_diff_report_path
97
- dir = "target/reports/diff/#{@name}"
113
+ def get_project_target_dir(branch_name)
114
+ branch_filename = PmdBranchDetail.branch_filename(branch_name)
115
+ dir = "target/reports/#{branch_filename}/#{@name}"
98
116
  FileUtils.mkdir_p(dir) unless File.directory?(dir)
99
117
  dir
100
118
  end
101
119
 
102
- def diff_report_index_path
103
- "#{target_diff_report_path}/index.html"
104
- end
105
-
106
- def diff_report_index_ref_path
107
- "./#{name}/index.html"
108
- end
109
-
110
- def diffs_exist?
111
- @report_diff.nil? ? false : @report_diff.diffs_exist?
112
- end
113
-
114
- def introduce_new_errors?
115
- @report_diff.nil? ? false : @report_diff.introduce_new_errors?
116
- end
117
-
118
- def removed_errors_size
119
- @report_diff.removed_errors_size
120
- end
121
-
122
- def new_errors_size
123
- @report_diff.new_errors_size
124
- end
125
-
126
- def removed_violations_size
127
- @report_diff.removed_violations_size
128
- end
129
-
130
- def new_violations_size
131
- @report_diff.new_violations_size
132
- end
133
-
134
- def removed_configerrors_size
135
- @report_diff.removed_configerrors_size
136
- end
137
-
138
- def new_configerrors_size
139
- @report_diff.new_configerrors_size
120
+ def compute_report_diff(base_branch, patch_branch, filter_set)
121
+ self.report_diff = build_report_diff(get_pmd_report_path(base_branch),
122
+ get_pmd_report_path(patch_branch),
123
+ get_report_info_path(base_branch),
124
+ get_report_info_path(patch_branch),
125
+ filter_set)
140
126
  end
141
127
  end
142
128
  end
@@ -1,157 +1,251 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PmdTester
4
- # This class represents all the diff report information,
5
- # including the summary information of the original pmd reports,
6
- # as well as the specific information of the diff report.
7
- class ReportDiff
8
- include PmdTester
9
-
10
- attr_accessor :base_violations_size
11
- attr_accessor :patch_violations_size
12
- attr_accessor :new_violations_size
13
- attr_accessor :removed_violations_size
14
- attr_accessor :violation_diffs_size
4
+ # A bunch of counters to summarize differences
5
+ class RunningDiffCounters
6
+ attr_accessor :changed, :new, :removed, :patch_total, :base_total
15
7
 
16
- attr_accessor :base_errors_size
17
- attr_accessor :patch_errors_size
18
- attr_accessor :new_errors_size
19
- attr_accessor :removed_errors_size
20
- attr_accessor :error_diffs_size
8
+ def initialize(base_total)
9
+ @base_total = base_total
10
+ @patch_total = 0
21
11
 
22
- attr_accessor :base_configerrors_size
23
- attr_accessor :patch_configerrors_size
24
- attr_accessor :new_configerrors_size
25
- attr_accessor :removed_configerrors_size
26
- attr_accessor :configerrors_diffs_size
27
-
28
- attr_accessor :base_execution_time
29
- attr_accessor :patch_execution_time
30
- attr_accessor :diff_execution_time
12
+ @new = @removed = @changed = 0
13
+ end
31
14
 
32
- attr_accessor :base_timestamp
33
- attr_accessor :patch_timestamp
15
+ def changed_total
16
+ new + removed + changed
17
+ end
34
18
 
35
- attr_accessor :violation_diffs
36
- attr_accessor :error_diffs
37
- attr_accessor :configerrors_diffs
19
+ def merge!(other)
20
+ self.changed += other.changed
21
+ self.new += other.new
22
+ self.removed += other.removed
23
+ self.base_total += other.base_total
24
+ self.patch_total += other.patch_total
25
+ end
38
26
 
39
- def initialize
40
- init_violations
41
- init_errors
42
- init_configerrors
27
+ def to_h
28
+ {
29
+ changed: changed,
30
+ new: new,
31
+ removed: removed,
32
+ base_total: base_total,
33
+ patch_total: patch_total
34
+ }
35
+ end
43
36
 
44
- @base_execution_time = 0
45
- @patch_execution_time = 0
46
- @diff_execution_time = 0
37
+ def to_s
38
+ "RunningDiffCounters[#{to_h}]"
39
+ end
40
+ end
47
41
 
48
- @base_timestamp = ''
49
- @patch_timestamp = ''
42
+ # Simple info about a rule, collected by the report xml parser
43
+ class RuleInfo
44
+ attr_reader :name, :info_url
50
45
 
51
- @violation_diffs = {}
52
- @error_diffs = {}
53
- @configerrors_diffs = {}
46
+ def initialize(name, info_url)
47
+ @name = name
48
+ @info_url = info_url
54
49
  end
50
+ end
55
51
 
56
- def init_violations
57
- @base_violations_size = 0
58
- @patch_violations_size = 0
59
- @new_violations_size = 0
60
- @removed_violations_size = 0
61
- @violation_diffs_size = 0
52
+ # A full report, created by the report XML parser,
53
+ # can be diffed with another report into a ReportDiff
54
+ class Report
55
+ attr_reader :violations_by_file,
56
+ :errors_by_file,
57
+ :configerrors_by_rule,
58
+ :exec_time,
59
+ :timestamp,
60
+ :file
61
+
62
+ def initialize(report_document: nil,
63
+ file: '',
64
+ exec_time: 0,
65
+ timestamp: '0')
66
+ initialize_empty
67
+ initialize_with_report_document report_document unless report_document.nil?
68
+ @exec_time = exec_time
69
+ @timestamp = timestamp
70
+ @file = file
62
71
  end
63
72
 
64
- def init_errors
65
- @base_errors_size = 0
66
- @patch_errors_size = 0
67
- @new_errors_size = 0
68
- @removed_errors_size = 0
69
- @error_diffs_size = 0
73
+ def self.empty
74
+ new
70
75
  end
71
76
 
72
- def init_configerrors
73
- @base_configerrors_size = 0
74
- @patch_configerrors_size = 0
75
- @new_configerrors_size = 0
76
- @removed_configerrors_size = 0
77
- @configerrors_diffs_size = 0
77
+ def rule_summaries
78
+ summary = {}
79
+ @violations_by_file.each_value do |violation|
80
+ unless summary.key?(violation.rule_name)
81
+ summary[violation.rule_name] = {
82
+ 'name' => violation.rule_name,
83
+ 'info_url' => violation.info_url,
84
+ 'count' => 0
85
+ }
86
+ end
87
+ summary[violation.rule_name]['count'] += 1
88
+ end
89
+
90
+ summary.values
78
91
  end
79
92
 
80
- def self.comparable?(errors)
81
- errors.size == 2 && errors[0].branch != errors[1].branch
93
+ private
94
+
95
+ def initialize_with_report_document(report_document)
96
+ @violations_by_file = report_document.violations
97
+ @errors_by_file = report_document.errors
98
+ @configerrors_by_rule = report_document.configerrors
99
+
100
+ PmdTester.logger.debug("Loaded #{@violations_by_file.total_size} violations " \
101
+ "in #{@violations_by_file.num_files} files")
102
+ PmdTester.logger.debug("Loaded #{@errors_by_file.total_size} errors " \
103
+ "in #{@errors_by_file.num_files} files")
104
+ PmdTester.logger.debug("Loaded #{@configerrors_by_rule.size} config errors")
82
105
  end
83
106
 
84
- def diffs_exist?
85
- !error_diffs_size.zero? || !violation_diffs_size.zero? || !configerrors_diffs_size.zero?
107
+ def initialize_empty
108
+ @violations_by_file = CollectionByFile.new
109
+ @errors_by_file = CollectionByFile.new
110
+ @configerrors_by_rule = {}
86
111
  end
112
+ end
113
+
114
+ # This class represents all the diff report information,
115
+ # including the summary information of the original pmd reports,
116
+ # as well as the specific information of the diff report.
117
+ class ReportDiff
118
+ include PmdTester
119
+
120
+ attr_reader :error_counts
121
+ attr_reader :violation_counts
122
+ attr_reader :configerror_counts
123
+
124
+ attr_accessor :violation_diffs_by_file
125
+ attr_accessor :error_diffs_by_file
126
+ attr_accessor :configerror_diffs_by_rule
127
+
128
+ attr_accessor :rule_infos_union
129
+ attr_accessor :base_report
130
+ attr_accessor :patch_report
131
+
132
+ def initialize(base_report:, patch_report:)
133
+ @violation_counts = RunningDiffCounters.new(base_report.violations_by_file.total_size)
134
+ @error_counts = RunningDiffCounters.new(base_report.errors_by_file.total_size)
135
+ @configerror_counts = RunningDiffCounters.new(base_report.configerrors_by_rule.values.flatten.length)
87
136
 
88
- def calculate_violations(base_violations, patch_violations)
89
- @base_violations_size = base_violations.violations_size
90
- @patch_violations_size = patch_violations.violations_size
91
- violation_diffs = build_diffs(base_violations.violations, patch_violations.violations)
92
- @violation_diffs = violation_diffs
93
- @new_violations_size, @removed_violations_size = get_diffs_size(violation_diffs)
94
- @violation_diffs_size = @new_violations_size + @removed_violations_size
137
+ @violation_diffs_by_file = {}
138
+ @error_diffs_by_file = {}
139
+ @configerror_diffs_by_rule = {}
140
+
141
+ @rule_infos_union = {}
142
+ @base_report = base_report
143
+ @patch_report = patch_report
144
+
145
+ @violation_diffs_by_rule = {}
146
+ diff_with(patch_report)
95
147
  end
96
148
 
97
- def calculate_errors(base_errors, patch_errors)
98
- @base_errors_size = base_errors.errors_size
99
- @patch_errors_size = patch_errors.errors_size
100
- error_diffs = build_diffs(base_errors.errors, patch_errors.errors)
101
- @error_diffs = error_diffs
102
- @new_errors_size, @removed_errors_size = get_diffs_size(error_diffs)
103
- @error_diffs_size = @new_errors_size + @removed_errors_size
149
+ def rule_summaries
150
+ @violation_diffs_by_rule.map do |(rule, counters)|
151
+ {
152
+ 'name' => rule,
153
+ 'info_url' => @rule_infos_union[rule].info_url,
154
+ **counters.to_h.transform_keys(&:to_s)
155
+ }
156
+ end
104
157
  end
105
158
 
106
- def calculate_configerrors(base_configerrors, patch_configerrors)
107
- @base_configerrors_size = base_configerrors.size
108
- @patch_configerrors_size = patch_configerrors.size
109
- configerrors_diffs = build_diffs(base_configerrors.errors, patch_configerrors.errors)
110
- @configerrors_diffs = configerrors_diffs
111
- @new_configerrors_size, @removed_configerrors_size = get_diffs_size(configerrors_diffs)
112
- @configerrors_diffs_size = @new_configerrors_size + @removed_configerrors_size
159
+ private
160
+
161
+ def diff_with(patch_report)
162
+ @violation_counts.patch_total = patch_report.violations_by_file.total_size
163
+ @error_counts.patch_total = patch_report.errors_by_file.total_size
164
+ @configerror_counts.patch_total = patch_report.configerrors_by_rule.values.flatten.length
165
+
166
+ @violation_diffs_by_file = build_diffs(@violation_counts, &:violations_by_file)
167
+ count(@violation_diffs_by_file) { |v| getvdiff(v.rule_name) } # record the diffs in the rule counter
168
+
169
+ @error_diffs_by_file = build_diffs(@error_counts, &:errors_by_file)
170
+ @configerror_diffs_by_rule = build_diffs(@configerror_counts, &:configerrors_by_rule)
171
+
172
+ count_by_rule(@base_report.violations_by_file, base: true)
173
+ count_by_rule(@patch_report.violations_by_file, base: false)
174
+ self
113
175
  end
114
176
 
115
- def calculate_details(base_info, patch_info)
116
- base_details = PmdReportDetail.new
117
- base_details.load(base_info) unless base_info.nil?
118
- patch_details = PmdReportDetail.new
119
- patch_details.load(patch_info) unless patch_info.nil?
177
+ def record_rule_info(violation)
178
+ return if @rule_infos_union.key?(violation.rule_name)
179
+
180
+ @rule_infos_union[violation.rule_name] = RuleInfo.new(violation.rule_name, violation.info_url)
181
+ end
120
182
 
121
- @base_execution_time = base_details.format_execution_time
122
- @patch_execution_time = patch_details.format_execution_time
123
- @diff_execution_time =
124
- PmdReportDetail.convert_seconds(base_details.execution_time -
125
- patch_details.execution_time)
183
+ def getvdiff(rule_name)
184
+ @violation_diffs_by_rule.fetch(rule_name) do |_|
185
+ @violation_diffs_by_rule[rule_name] = RunningDiffCounters.new(0)
186
+ end
187
+ end
126
188
 
127
- @base_timestamp = base_details.timestamp
128
- @patch_timestamp = patch_details.timestamp
129
- [base_details, patch_details]
189
+ def count_by_rule(violations_h, base:)
190
+ violations_h.each_value do |v|
191
+ record_rule_info(v)
192
+ rule_diff = getvdiff(v.rule_name)
193
+ if base
194
+ rule_diff.base_total += 1
195
+ else
196
+ rule_diff.patch_total += 1
197
+ end
198
+ end
130
199
  end
131
200
 
132
- def build_diffs(base_hash, patch_hash)
133
- diffs = base_hash.merge(patch_hash) do |_key, base_value, patch_value|
201
+ def build_diffs(counters, &getter)
202
+ base_hash = getter.yield(@base_report)
203
+ patch_hash = getter.yield(@patch_report)
204
+ # Keys are filenames
205
+ # Values are lists of violations/errors
206
+ diffs = base_hash.to_h.merge(patch_hash.to_h) do |_key, base_value, patch_value|
207
+ # make the difference of values
134
208
  (base_value | patch_value) - (base_value & patch_value)
135
209
  end
136
210
 
137
211
  diffs.delete_if do |_key, value|
138
212
  value.empty?
139
213
  end
214
+
215
+ merge_changed_items(diffs)
216
+ count(diffs) { |_| counters }
217
+ diffs
140
218
  end
141
219
 
142
- def get_diffs_size(diffs_hash)
143
- new_size = 0
144
- removed_size = 0
145
- diffs_hash.each_value do |value|
146
- value.each do |item|
147
- item.branch.eql?(BASE) ? removed_size += 1 : new_size += 1
220
+ # @param diff_h a hash { filename => list[violation]}, containing those that changed
221
+ # in case of config errors it's a hash { rulename => list[configerror] }
222
+ def merge_changed_items(diff_h)
223
+ diff_h.each do |fname, different|
224
+ different.sort_by!(&:sort_key)
225
+ diff_h[fname] = different.delete_if do |v|
226
+ v.branch == BASE &&
227
+ # try_merge will set v2.changed = true if it succeeds
228
+ different.any? { |v2| v2.try_merge?(v) }
148
229
  end
149
230
  end
150
- [new_size, removed_size]
151
231
  end
152
232
 
153
- def introduce_new_errors?
154
- !@new_errors_size.zero? || !@new_configerrors_size.zero?
233
+ def count(item_h)
234
+ item_h = { '' => item_h } if item_h.is_a?(Array)
235
+
236
+ item_h.each do |_k, items|
237
+ items.each do |item|
238
+ counter = yield item
239
+
240
+ if item.changed?
241
+ counter.changed += 1
242
+ elsif item.branch.eql?(BASE)
243
+ counter.removed += 1
244
+ else
245
+ counter.new += 1
246
+ end
247
+ end
248
+ end
155
249
  end
156
250
  end
157
251
  end