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
@@ -9,8 +9,9 @@ module PmdTester
9
9
  # Attention: we only consider java rulesets now.
10
10
  class RuleSetBuilder
11
11
  include PmdTester
12
- ALL_RULE_SETS = Set['bestpractices', 'codestyle', 'design', 'documentation',
13
- 'errorprone', 'multithreading', 'performance', 'security'].freeze
12
+ ALL_CATEGORIES = Set['bestpractices.xml', 'codestyle.xml', 'design.xml', 'documentation.xml',
13
+ 'errorprone.xml', 'multithreading.xml', 'performance.xml',
14
+ 'security.xml'].freeze
14
15
  PATH_TO_PMD_JAVA_BASED_RULES =
15
16
  'pmd-java/src/main/java/net/sourceforge/pmd/lang/java/rule'
16
17
  PATH_TO_PMD_XPATH_BASED_RULES = 'pmd-java/src/main/resources/category/java'
@@ -25,19 +26,39 @@ module PmdTester
25
26
 
26
27
  def build
27
28
  filenames = diff_filenames
28
- rule_sets = get_rule_sets(filenames)
29
- output_filter_set(rule_sets)
30
- build_config_file(rule_sets)
31
- logger.debug "Dynamic configuration: #{[rule_sets]}"
32
- rule_sets
29
+ rule_refs = get_rule_refs(filenames)
30
+ output_filter_set(rule_refs)
31
+ build_config_file(rule_refs)
32
+ logger.debug "Dynamic configuration: #{[rule_refs]}"
33
+ rule_refs
33
34
  end
34
35
 
35
- def output_filter_set(rule_sets)
36
- if rule_sets == ALL_RULE_SETS
37
- # if `rule_sets` contains all rule sets, than no need to filter the baseline
38
- @options.filter_set = nil
36
+ def calculate_filter_set
37
+ output_filter_set(ALL_CATEGORIES)
38
+ end
39
+
40
+ def output_filter_set(rule_refs)
41
+ if rule_refs == ALL_CATEGORIES
42
+ if @options.mode == Options::ONLINE
43
+ @options.filter_set = Set[]
44
+ doc = File.open(@options.patch_config) { |f| Nokogiri::XML(f) }
45
+ rules = doc.css('ruleset rule')
46
+ rules.each do |r|
47
+ ref = r.attributes['ref'].content
48
+ ref.delete_prefix!('category/java/')
49
+ @options.filter_set.add(ref)
50
+ end
51
+
52
+ logger.debug "Using filter based on patch config #{@options.patch_config}: " \
53
+ "#{@options.filter_set}"
54
+ else
55
+ # if `rule_refs` contains all categories, then no need to filter the baseline
56
+ logger.debug 'No filter when comparing patch to baseline'
57
+ @options.filter_set = nil
58
+ end
39
59
  else
40
- @options.filter_set = rule_sets
60
+ logger.debug "Filter is now #{rule_refs}"
61
+ @options.filter_set = rule_refs
41
62
  end
42
63
  end
43
64
 
@@ -53,58 +74,86 @@ module PmdTester
53
74
  filenames.split("\n")
54
75
  end
55
76
 
56
- def get_rule_sets(filenames)
57
- rule_sets = Set[]
77
+ def get_rule_refs(filenames)
78
+ categories, rules = determine_categories_rules(filenames)
79
+ logger.debug "Categories: #{categories}"
80
+ logger.debug "Rules: #{rules}"
81
+
82
+ # filter out all individual rules that are already covered by a complete category
83
+ categories.each do |cat|
84
+ rules.delete_if { |e| e.start_with?(cat) }
85
+ end
86
+
87
+ refs = Set[]
88
+ refs.merge(categories)
89
+ refs.merge(rules)
90
+ refs
91
+ end
92
+
93
+ def determine_categories_rules(filenames)
94
+ categories = Set[]
95
+ rules = Set[]
58
96
  filenames.each do |filename|
59
- match_data = %r{#{PATH_TO_PMD_JAVA_BASED_RULES}/([^/]+)/[^/]+Rule.java}.match(filename)
60
- if match_data.nil?
61
- match_data = %r{#{PATH_TO_PMD_XPATH_BASED_RULES}/([^/]+).xml}.match(filename)
97
+ match_data = check_single_filename(filename)
98
+
99
+ unless match_data.nil?
100
+ if match_data.size == 2
101
+ categories.add("#{match_data[1]}.xml")
102
+ else
103
+ rules.add("#{match_data[1]}.xml/#{match_data[2]}")
104
+ end
62
105
  end
63
106
 
64
- category = if match_data.nil?
65
- nil
66
- else
67
- match_data[1]
68
- end
107
+ next unless match_data.nil?
69
108
 
70
- if category.nil?
71
- rule_sets = ALL_RULE_SETS
72
- break
73
- else
74
- rule_sets.add(category)
75
- end
109
+ logger.debug "Change doesn't match specific rule/category - enable all rules"
110
+ categories = ALL_CATEGORIES
111
+ rules.clear
112
+ break
76
113
  end
77
- rule_sets
114
+ [categories, rules]
115
+ end
116
+
117
+ def check_single_filename(filename)
118
+ logger.debug "Checking #{filename}"
119
+ match_data = %r{#{PATH_TO_PMD_JAVA_BASED_RULES}/([^/]+)/([^/]+)Rule.java}.match(filename)
120
+ match_data = %r{#{PATH_TO_PMD_XPATH_BASED_RULES}/([^/]+).xml}.match(filename) if match_data.nil?
121
+ logger.debug "Matches: #{match_data.inspect}"
122
+ match_data
78
123
  end
79
124
 
80
- def build_config_file(rule_sets)
81
- if rule_sets.empty?
125
+ def build_config_file(rule_refs)
126
+ if rule_refs.empty?
82
127
  logger.info NO_JAVA_RULES_CHANGED_MESSAGE
83
128
  return
84
129
  end
85
130
 
86
- doc = Nokogiri::XML(File.read(PATH_TO_ALL_JAVA_RULES))
87
- doc.search('rule').each do |rule|
88
- rule.remove unless match_ref?(rule, rule_sets)
131
+ if rule_refs == ALL_CATEGORIES
132
+ logger.debug 'All rules are used. Not generating a dynamic ruleset.'
133
+ logger.debug "Using the configured/default ruleset base_config=#{@options.base_config} "\
134
+ "patch_config=#{@options.patch_config}"
135
+ return
89
136
  end
90
137
 
91
- description = doc.at_css('description')
92
- description.content = 'The ruleset generated by PmdTester dynamically'
93
-
94
- write_dynamic_file(doc)
138
+ write_dynamic_file(rule_refs)
95
139
  end
96
140
 
97
- def match_ref?(rule_node, rule_sets)
98
- rule_sets.each do |rule_set|
99
- return true unless rule_node['ref'].index(rule_set).nil?
141
+ def write_dynamic_file(rule_refs)
142
+ logger.debug "Generating dynamic configuration for: #{[rule_refs]}"
143
+ builder = Nokogiri::XML::Builder.new(encoding: 'UTF-8') do |xml|
144
+ xml.ruleset('xmlns' => 'http://pmd.sourceforge.net/ruleset/2.0.0',
145
+ 'xmlns:xsi' => 'http://www.w3.org/2001/XMLSchema-instance',
146
+ 'xsi:schemaLocation' => 'http://pmd.sourceforge.net/ruleset/2.0.0 https://pmd.sourceforge.io/ruleset_2_0_0.xsd',
147
+ 'name' => 'Dynamic PmdTester Ruleset') do
148
+ xml.description 'The ruleset generated by PmdTester dynamically'
149
+ rule_refs.each do |entry|
150
+ xml.rule('ref' => "category/java/#{entry}")
151
+ end
152
+ end
100
153
  end
101
-
102
- false
103
- end
104
-
105
- def write_dynamic_file(doc)
154
+ doc = builder.to_xml(indent: 4, encoding: 'UTF-8')
106
155
  File.open(PATH_TO_DYNAMIC_CONFIG, 'w') do |x|
107
- x << doc.to_s.gsub(/\n\s+\n/, "\n")
156
+ x << doc.gsub(/\n\s+\n/, "\n")
108
157
  end
109
158
  @options.base_config = PATH_TO_DYNAMIC_CONFIG
110
159
  @options.patch_config = PATH_TO_DYNAMIC_CONFIG
@@ -6,16 +6,16 @@ module PmdTester
6
6
  # Helper class that provides a simple progress logging
7
7
  class SimpleProgressLogger
8
8
  include PmdTester
9
- def initialize(project_name)
10
- @project_name = project_name
9
+ def initialize(task_name)
10
+ @task_name = task_name
11
11
  end
12
12
 
13
13
  def start
14
- logger.info "Starting to generate #{@project_name}'s PMD report"
14
+ logger.info "Starting #{@task_name}"
15
15
  message_counter = 1
16
16
  @scheduler = Rufus::Scheduler.new
17
17
  @scheduler.every '2m' do
18
- logger.info "Still generating #{@project_name}'s PMD report (#{message_counter})..."
18
+ logger.info "Still #{@task_name} (#{message_counter})..."
19
19
  message_counter += 1
20
20
  end
21
21
  end
@@ -2,159 +2,91 @@
2
2
 
3
3
  module PmdTester
4
4
  # Building summary report to show the details about projects and pmd branchs
5
- class SummaryReportBuilder < HtmlReportBuilder
5
+ class SummaryReportBuilder
6
6
  include PmdTester
7
- REPORT_DIR = 'target/reports/diff'
8
- BASE_CONFIG_PATH = 'target/reports/diff/base_config.xml'
9
- PATCH_CONFIG_PATH = 'target/reports/diff/patch_config.xml'
10
- INDEX_PATH = 'target/reports/diff/index.html'
11
-
12
- def build(projects, base_name, patch_name)
13
- @projects = projects
14
- @base_details = get_branch_details(base_name)
15
- @patch_details = get_branch_details(patch_name)
16
-
17
- FileUtils.mkdir_p(REPORT_DIR) unless File.directory?(REPORT_DIR)
18
- index = File.new(INDEX_PATH, 'w')
19
-
20
- html_report = build_html_report('Summary report')
21
- copy_css(REPORT_DIR)
22
-
23
- index.puts html_report
24
- index.close
25
-
26
- logger.info 'Built summary report successfully!'
27
- end
28
-
29
- def get_branch_details(branch_name)
30
- details = PmdBranchDetail.new(branch_name)
31
- details.load
32
- end
7
+ include LiquidRenderer
8
+ include ProjectHasher
33
9
 
34
- def build_body(doc)
35
- build_branch_details_section(doc)
36
- build_projects_section(doc)
37
- end
10
+ REPORT_DIR = 'target/reports/diff'
11
+ BASE_CONFIG_NAME = 'base_config.xml'
12
+ PATCH_CONFIG_NAME = 'patch_config.xml'
38
13
 
39
- def build_branch_details_section(doc)
40
- doc.div(class: 'section', id: 'branchdetails') do
41
- doc.h2 'Branch details:'
42
- build_branch_details_table(doc)
14
+ def write_all_projects(projects, base_details, patch_details)
15
+ projects.each do |project|
16
+ process_project(project, "#{REPORT_DIR}/#{project.name}")
43
17
  end
44
- end
18
+ logger.info 'Built all difference reports successfully!'
45
19
 
46
- def build_branch_details_table(doc)
47
- doc.table(class: 'bodyTable', border: '0') do
48
- build_branch_table_head(doc)
49
- build_branch_table_body(doc)
50
- end
20
+ FileUtils.mkdir_p(REPORT_DIR)
21
+ write_structure(REPORT_DIR)
22
+ copy_configs(REPORT_DIR, base_details, patch_details)
23
+ write_index(REPORT_DIR, base_details, patch_details, projects)
24
+ logger.info "Built summary report successfully in #{REPORT_DIR}!"
51
25
  end
52
26
 
53
- def build_branch_table_head(doc)
54
- doc.thead do
55
- doc.tr do
56
- doc.th 'Item'
57
- doc.th 'base'
58
- doc.th 'patch'
59
- end
60
- end
61
- end
27
+ private
62
28
 
63
- def build_branch_table_body(doc)
64
- doc.tbody do
65
- build_branch_table_row(doc, 'branch name', @base_details.branch_name,
66
- @patch_details.branch_name)
67
- build_branch_table_row(doc, 'branch last commit sha', @base_details.branch_last_sha,
68
- @patch_details.branch_last_sha)
69
- build_branch_table_row(doc, 'branch last commit message', @base_details.branch_last_message,
70
- @patch_details.branch_last_message)
71
- build_branch_table_row(doc, 'total execution time', @base_details.format_execution_time,
72
- @patch_details.format_execution_time)
73
- build_branch_table_row(doc, 'jdk version', @base_details.jdk_version,
74
- @patch_details.jdk_version)
75
- build_branch_table_row(doc, 'language', @base_details.language,
76
- @patch_details.language)
77
- build_branch_config_table_row(doc)
78
- end
29
+ def process_project(project, dir)
30
+ logger.info "Rendering #{project.name}..."
31
+ LiquidProjectRenderer.new.write_project_index(project, dir)
79
32
  end
80
33
 
81
- def build_branch_config_table_row(doc)
82
- doc.tr do
83
- doc.td(class: 'c') { doc.text 'branch configuration' }
84
- base_config_src_path = @base_details.target_branch_config_path
85
- copy_branch_config_file(base_config_src_path, BASE_CONFIG_PATH)
86
- doc.td(class: 'a') do
87
- doc.a(href: './base_config.xml') { doc.text 'base config' }
88
- end
89
- patch_config_stc_path = @patch_details.target_branch_config_path
90
- FileUtils.cp(patch_config_stc_path, PATCH_CONFIG_PATH)
91
- doc.td(class: 'b') do
92
- doc.a(href: './patch_config.xml') { doc.text 'patch config' }
93
- end
94
- end
34
+ def write_structure(target_root)
35
+ logger.info 'Copying resources...'
36
+ copy_resource('css', target_root)
37
+ copy_resource('js', target_root)
95
38
  end
96
39
 
97
- def copy_branch_config_file(src, dest)
98
- FileUtils.cp(src, dest) if File.exist?(src)
40
+ def copy_configs(target_root, base_details, patch_details)
41
+ copy_file(base_details.target_branch_config_path, "#{target_root}/#{BASE_CONFIG_NAME}")
42
+ copy_file(patch_details.target_branch_config_path, "#{target_root}/#{PATCH_CONFIG_NAME}")
99
43
  end
100
44
 
101
- def build_branch_table_row(doc, item, base, patch)
102
- doc.tr do
103
- doc.td(class: 'c') { doc.text item }
104
- doc.td(class: 'a') { doc.text base }
105
- doc.td(class: 'b') { doc.text patch }
106
- end
45
+ def copy_file(src, dest)
46
+ FileUtils.cp(src, dest) if File.exist?(src)
107
47
  end
108
48
 
109
- def build_projects_section(doc)
110
- doc.div(class: 'section', id: 'projects') do
111
- doc.h2 'Projects:'
112
- build_projects_table(doc)
49
+ def write_index(target_root, base_details, patch_details, projects)
50
+ projects = projects.map do |p|
51
+ {
52
+ 'name' => p.name,
53
+ 'tag' => p.tag,
54
+ 'report_url' => "./#{p.name}/index.html",
55
+ **report_diff_to_h(p.report_diff)
56
+ }
113
57
  end
114
- end
115
58
 
116
- def build_projects_table(doc)
117
- doc.table(class: 'bodyTable', border: '0') do
118
- build_projects_table_head(doc)
119
- build_projects_table_body(doc)
120
- end
59
+ env = {
60
+ 'comparison_url' => create_comparison_url(base_details, patch_details),
61
+ 'base' => to_liquid(base_details, BASE_CONFIG_NAME),
62
+ 'patch' => to_liquid(patch_details, PATCH_CONFIG_NAME),
63
+ 'projects' => projects
64
+ }
65
+ logger.info 'Writing /index.html...'
66
+ render_and_write('project_index.html', "#{target_root}/index.html", env)
121
67
  end
122
68
 
123
- def build_projects_table_head(doc)
124
- doc.thead do
125
- doc.tr do
126
- doc.th 'project name'
127
- doc.th 'project branch/tag'
128
- doc.th 'removed|new errors size'
129
- doc.th 'removed|new violations size'
130
- doc.th 'removed|new configerrors size'
131
- end
132
- end
69
+ def create_comparison_url(base_details, patch_details)
70
+ base = CGI.escape(base_details.branch_name)
71
+ patch = CGI.escape(patch_details.branch_last_sha)
72
+ "https://github.com/pmd/pmd/compare/#{base}...#{patch}"
133
73
  end
134
74
 
135
- def build_projects_table_body(doc)
136
- doc.tbody do
137
- @projects.each do |project|
138
- doc.tr do
139
- doc.td do
140
- doc.a(href: project.diff_report_index_ref_path) { doc.text project.name }
141
- end
142
- doc.td project.tag
143
- doc.td do
144
- build_table_content_for(doc, project.removed_errors_size,
145
- project.new_errors_size)
146
- end
147
- doc.td do
148
- build_table_content_for(doc, project.removed_violations_size,
149
- project.new_violations_size)
150
- end
151
- doc.td do
152
- build_table_content_for(doc, project.removed_configerrors_size,
153
- project.new_configerrors_size)
154
- end
155
- end
156
- end
157
- end
75
+ def to_liquid(details, config_name)
76
+ {
77
+ 'tree_url' => "https://github.com/pmd/pmd/tree/#{CGI.escape(details.branch_last_sha)}",
78
+ 'name' => details.branch_name,
79
+ 'tip' => {
80
+ 'sha' => details.branch_last_sha,
81
+ 'message' => details.branch_last_message
82
+ },
83
+ 'timestamp' => details.timestamp,
84
+ 'execution_time' => PmdReportDetail.convert_seconds(details.execution_time),
85
+ 'jdk_info' => details.jdk_version,
86
+ 'locale' => details.language,
87
+ 'config_url' => config_name,
88
+ 'pr_number' => details.pull_request
89
+ }
158
90
  end
159
91
  end
160
92
  end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PmdTester
4
+ # A collection of things, grouped by file.
5
+ #
6
+ # (Note: this replaces PmdErrors and PmdViolations)
7
+ class CollectionByFile
8
+ def initialize
9
+ # a hash of filename -> [list of items]
10
+ @hash = Hash.new([])
11
+ @total = 0
12
+ end
13
+
14
+ def add_all(filename, values)
15
+ return if values.empty?
16
+
17
+ if @hash.key?(filename)
18
+ @hash[filename].concat(values)
19
+ else
20
+ @hash[filename] = values
21
+ end
22
+ @total += values.size
23
+ end
24
+
25
+ def total_size
26
+ @total
27
+ end
28
+
29
+ def all_files
30
+ @hash.keys
31
+ end
32
+
33
+ def num_files
34
+ @hash.size
35
+ end
36
+
37
+ def all_values
38
+ @hash.values.flatten
39
+ end
40
+
41
+ def each_value(&block)
42
+ @hash.each_value do |vs|
43
+ vs.each(&block)
44
+ end
45
+ end
46
+
47
+ def [](fname)
48
+ @hash[fname]
49
+ end
50
+
51
+ def to_h
52
+ @hash
53
+ end
54
+ end
55
+ end