pmdtester 1.0.0.pre.beta2 → 1.1.1

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 +67 -0
  3. data/.ci/files/env.gpg +1 -0
  4. data/.ci/inc/install-openjdk.inc +26 -0
  5. data/.ci/manual-integration-tests.sh +20 -0
  6. data/.github/workflows/build.yml +39 -0
  7. data/.github/workflows/manual-integration-tests.yml +32 -0
  8. data/.gitignore +9 -0
  9. data/.hoerc +1 -0
  10. data/.rubocop.yml +21 -2
  11. data/.rubocop_todo.yml +7 -8
  12. data/.ruby-version +1 -0
  13. data/Gemfile +1 -13
  14. data/History.md +108 -0
  15. data/Manifest.txt +60 -0
  16. data/README.rdoc +130 -7
  17. data/Rakefile +31 -17
  18. data/bin/pmdtester +1 -1
  19. data/config/all-java.xml +1 -1
  20. data/config/design.xml +1 -1
  21. data/config/projectlist_1_0_0.xsd +2 -1
  22. data/config/projectlist_1_1_0.xsd +31 -0
  23. data/lib/pmdtester.rb +48 -0
  24. data/lib/pmdtester/builders/liquid_renderer.rb +73 -0
  25. data/lib/pmdtester/builders/pmd_report_builder.rb +133 -64
  26. data/lib/pmdtester/builders/project_builder.rb +100 -0
  27. data/lib/pmdtester/builders/project_hasher.rb +126 -0
  28. data/lib/pmdtester/builders/rule_set_builder.rb +95 -51
  29. data/lib/pmdtester/builders/simple_progress_logger.rb +27 -0
  30. data/lib/pmdtester/builders/summary_report_builder.rb +62 -120
  31. data/lib/pmdtester/cmd.rb +15 -1
  32. data/lib/pmdtester/collection_by_file.rb +55 -0
  33. data/lib/pmdtester/parsers/options.rb +33 -10
  34. data/lib/pmdtester/parsers/pmd_report_document.rb +79 -29
  35. data/lib/pmdtester/parsers/projects_parser.rb +2 -6
  36. data/lib/pmdtester/pmd_branch_detail.rb +36 -13
  37. data/lib/pmdtester/pmd_configerror.rb +62 -0
  38. data/lib/pmdtester/pmd_error.rb +34 -34
  39. data/lib/pmdtester/pmd_report_detail.rb +10 -13
  40. data/lib/pmdtester/pmd_tester_utils.rb +57 -0
  41. data/lib/pmdtester/pmd_violation.rb +66 -26
  42. data/lib/pmdtester/project.rb +28 -25
  43. data/lib/pmdtester/report_diff.rb +194 -70
  44. data/lib/pmdtester/resource_locator.rb +4 -0
  45. data/lib/pmdtester/runner.rb +82 -57
  46. data/pmdtester.gemspec +64 -0
  47. data/resources/_includes/diff_pill_row.html +6 -0
  48. data/resources/css/bootstrap.min.css +7 -0
  49. data/resources/css/datatables.min.css +36 -0
  50. data/resources/css/pmd-tester.css +131 -0
  51. data/resources/js/bootstrap.min.js +7 -0
  52. data/resources/js/code-snippets.js +66 -0
  53. data/resources/js/datatables.min.js +726 -0
  54. data/resources/js/jquery-3.2.1.slim.min.js +4 -0
  55. data/resources/js/jquery.min.js +2 -0
  56. data/resources/js/popper.min.js +5 -0
  57. data/resources/js/project-report.js +136 -0
  58. data/resources/project_diff_report.html +205 -0
  59. data/resources/project_index.html +102 -0
  60. metadata +117 -44
  61. data/.travis.yml +0 -22
  62. data/lib/pmdtester/builders/diff_builder.rb +0 -35
  63. data/lib/pmdtester/builders/diff_report_builder.rb +0 -226
  64. data/lib/pmdtester/builders/html_report_builder.rb +0 -34
  65. data/lib/pmdtester/pmdtester.rb +0 -17
  66. data/resources/css/maven-base.css +0 -155
  67. data/resources/css/maven-theme.css +0 -171
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+ require 'tempfile'
5
+
6
+ module PmdTester
7
+ # Clones and builds the projects, that are configured in the project-list.xml
8
+ class ProjectBuilder
9
+ include PmdTester
10
+
11
+ def initialize(projects)
12
+ @projects = projects
13
+ end
14
+
15
+ def clone_projects
16
+ logger.info 'Cloning projects started'
17
+
18
+ @projects.each do |project|
19
+ logger.info "Start cloning #{project.name} repository"
20
+ path = project.local_source_path
21
+ clone_cmd = "#{project.type} clone #{project.connection} #{path}"
22
+ if File.exist?(path)
23
+ logger.warn "Skipping clone, project path #{path} already exists"
24
+ else
25
+ Cmd.execute(clone_cmd)
26
+ end
27
+
28
+ Dir.chdir(path) do
29
+ execute_reset_cmd(project.type, project.tag)
30
+ end
31
+ logger.info "Cloning #{project.name} completed"
32
+ end
33
+
34
+ logger.info 'Cloning projects completed'
35
+ end
36
+
37
+ def build_projects
38
+ logger.info 'Building projects started'
39
+
40
+ @projects.each do |project|
41
+ path = project.local_source_path
42
+ Dir.chdir(path) do
43
+ progress_logger = SimpleProgressLogger.new("building #{project.name} in #{path}")
44
+ progress_logger.start
45
+ prepare_project(project)
46
+ progress_logger.stop
47
+ end
48
+ logger.info "Building #{project.name} completed"
49
+ end
50
+
51
+ logger.info 'Building projects completed'
52
+ end
53
+
54
+ private
55
+
56
+ def prepare_project(project)
57
+ # Note: current working directory is the project directory,
58
+ # where the source code has been cloned to
59
+ if project.build_command
60
+ logger.debug "Executing build-command: #{project.build_command}"
61
+ run_as_script(Dir.getwd, project.build_command)
62
+ end
63
+ if project.auxclasspath_command
64
+ logger.debug "Executing auxclasspath-command: #{project.auxclasspath_command}"
65
+ auxclasspath = run_as_script(Dir.getwd, project.auxclasspath_command)
66
+ project.auxclasspath = "-auxclasspath #{auxclasspath}"
67
+ else
68
+ project.auxclasspath = ''
69
+ end
70
+ end
71
+
72
+ def run_as_script(path, command)
73
+ script = Tempfile.new(['pmd-regression-', '.sh'], path)
74
+ logger.debug "Creating script #{script.path}"
75
+ begin
76
+ script.write(command)
77
+ script.close
78
+ shell = 'sh -xe'
79
+ if command.start_with?('#!')
80
+ shell = command.lines[0].chomp[2..] # remove leading "#!"
81
+ end
82
+ stdout = Cmd.execute("#{shell} #{script.path}")
83
+ ensure
84
+ script.unlink
85
+ end
86
+ stdout
87
+ end
88
+
89
+ def execute_reset_cmd(type, tag)
90
+ case type
91
+ when 'git'
92
+ reset_cmd = "git checkout #{tag}; git reset --hard #{tag}"
93
+ when 'hg'
94
+ reset_cmd = "hg up #{tag}"
95
+ end
96
+
97
+ Cmd.execute(reset_cmd)
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,126 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'differ'
4
+
5
+ module PmdTester
6
+ # Turn a project report into a hash that can be rendered somewhere else
7
+ module ProjectHasher
8
+ include PmdTester
9
+
10
+ def report_diff_to_h(rdiff)
11
+ {
12
+ 'violation_counts' => rdiff.violation_counts.to_h.transform_keys(&:to_s),
13
+ 'error_counts' => rdiff.error_counts.to_h.transform_keys(&:to_s),
14
+ 'configerror_counts' => rdiff.configerror_counts.to_h.transform_keys(&:to_s),
15
+
16
+ 'base_execution_time' => PmdReportDetail.convert_seconds(rdiff.base_report.exec_time),
17
+ 'patch_execution_time' => PmdReportDetail.convert_seconds(rdiff.patch_report.exec_time),
18
+ 'diff_execution_time' => PmdReportDetail.convert_seconds(rdiff.patch_report.exec_time -
19
+ rdiff.base_report.exec_time),
20
+
21
+ 'base_timestamp' => rdiff.base_report.timestamp,
22
+ 'patch_timestamp' => rdiff.patch_report.timestamp,
23
+
24
+ 'rule_diffs' => rdiff.rule_summaries
25
+ }
26
+ end
27
+
28
+ def errors_to_h(project)
29
+ errors = project.report_diff.error_diffs_by_file.values.flatten
30
+ errors.map { |e| error_to_hash(e, project) }
31
+ end
32
+
33
+ def configerrors_to_h(project)
34
+ configerrors = project.report_diff.configerror_diffs_by_rule.values.flatten
35
+ configerrors.map { |e| configerror_to_hash(e) }
36
+ end
37
+
38
+ def violations_to_hash(project)
39
+ filename_index = []
40
+ all_vs = []
41
+ project.report_diff.violation_diffs_by_file.each do |file, vs|
42
+ file_ref = filename_index.size
43
+ filename_index.push(project.get_local_path(file))
44
+ vs.each do |v|
45
+ all_vs.push(make_violation_hash(file_ref, v))
46
+ end
47
+ end
48
+
49
+ {
50
+ 'file_index' => filename_index,
51
+ 'violations' => all_vs
52
+ }
53
+ end
54
+
55
+ def link_template(project)
56
+ l_str = project.type == 'git' ? 'L' : 'l'
57
+ "#{project.webview_url}/{file}##{l_str}{line}"
58
+ end
59
+
60
+ def violation_type(violation)
61
+ if violation.changed?
62
+ '~'
63
+ elsif violation.branch == 'patch'
64
+ '+'
65
+ else
66
+ '-'
67
+ end
68
+ end
69
+
70
+ def make_violation_hash(file_ref, violation)
71
+ h = {
72
+ 't' => violation_type(violation),
73
+ 'l' => violation.line,
74
+ 'f' => file_ref,
75
+ 'r' => violation.rule_name,
76
+ 'm' => violation.changed? ? diff_fragments(violation) : violation.message
77
+ }
78
+ h['ol'] = violation.old_line if violation.changed? && violation.line != violation.old_line
79
+ h
80
+ end
81
+
82
+ def diff_fragments(violation)
83
+ diff = Differ.diff_by_word(violation.message, violation.old_message)
84
+ diff.format_as(:html)
85
+ end
86
+
87
+ def error_to_hash(error, project)
88
+ escaped_stacktrace = sanitize_stacktrace(error)
89
+ old_stacktrace = error.old_error.nil? ? nil : sanitize_stacktrace(error.old_error)
90
+
91
+ {
92
+ 'file_url' => project.get_webview_url(error.filename),
93
+ 'stack_trace_html' => escaped_stacktrace,
94
+ 'old_stack_trace_html' => old_stacktrace,
95
+ 'short_message' => error.short_message,
96
+ 'short_filename' => error.short_filename,
97
+ 'filename' => error.filename,
98
+ 'change_type' => change_type(error)
99
+ }
100
+ end
101
+
102
+ def sanitize_stacktrace(error)
103
+ CGI.escapeHTML(error.stack_trace)
104
+ .gsub(error.filename, '<span class="meta-var">$FILE</span>')
105
+ .gsub(/\w++(?=\(\w++\.java:\d++\))/, '<span class="stack-trace-method">\\0</span>')
106
+ end
107
+
108
+ def configerror_to_hash(configerror)
109
+ {
110
+ 'rule' => configerror.rulename,
111
+ 'message' => configerror.msg,
112
+ 'change_type' => change_type(configerror)
113
+ }
114
+ end
115
+
116
+ def change_type(item)
117
+ if item.branch == BASE
118
+ 'removed'
119
+ elsif item.changed?
120
+ 'changed'
121
+ else
122
+ 'added'
123
+ end
124
+ end
125
+ end
126
+ end
@@ -2,8 +2,6 @@
2
2
 
3
3
  require 'nokogiri'
4
4
  require 'set'
5
- require_relative '../cmd'
6
- require_relative '../resource_locator'
7
5
 
8
6
  module PmdTester
9
7
  # This class is responsible for generation dynamic configuration
@@ -11,8 +9,9 @@ module PmdTester
11
9
  # Attention: we only consider java rulesets now.
12
10
  class RuleSetBuilder
13
11
  include PmdTester
14
- ALL_RULE_SETS = Set['bestpractices', 'codestyle', 'design', 'documentation',
15
- '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
16
15
  PATH_TO_PMD_JAVA_BASED_RULES =
17
16
  'pmd-java/src/main/java/net/sourceforge/pmd/lang/java/rule'
18
17
  PATH_TO_PMD_XPATH_BASED_RULES = 'pmd-java/src/main/resources/category/java'
@@ -27,18 +26,35 @@ module PmdTester
27
26
 
28
27
  def build
29
28
  filenames = diff_filenames
30
- rule_sets = get_rule_sets(filenames)
31
- output_filter_set(rule_sets)
32
- build_config_file(rule_sets)
33
- logger.debug "Dynamic configuration: #{[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
34
34
  end
35
35
 
36
- def output_filter_set(rule_sets)
37
- if rule_sets == ALL_RULE_SETS
38
- # if `rule_sets` contains all rule sets, than no need to filter the baseline
39
- @options.filter_set = nil
36
+ def output_filter_set(rule_refs)
37
+ if rule_refs == ALL_CATEGORIES
38
+ if @options.mode == Options::ONLINE
39
+ @options.filter_set = Set[]
40
+ doc = File.open(@options.patch_config) { |f| Nokogiri::XML(f) }
41
+ rules = doc.css('ruleset rule')
42
+ rules.each do |r|
43
+ ref = r.attributes['ref'].content
44
+ ref.delete_prefix!('category/java/')
45
+ @options.filter_set.add(ref)
46
+ end
47
+
48
+ logger.debug "Using filter based on patch config #{@options.patch_config}: " \
49
+ "#{@options.filter_set}"
50
+ else
51
+ # if `rule_refs` contains all categories, then no need to filter the baseline
52
+ logger.debug 'No filter when comparing patch to baseline'
53
+ @options.filter_set = nil
54
+ end
40
55
  else
41
- @options.filter_set = rule_sets
56
+ logger.debug "Filter is now #{rule_refs}"
57
+ @options.filter_set = rule_refs
42
58
  end
43
59
  end
44
60
 
@@ -48,64 +64,92 @@ module PmdTester
48
64
  base = @options.base_branch
49
65
  patch = @options.patch_branch
50
66
  # We only need to support git here, since PMD's repo is using git.
51
- diff_cmd = "git diff --name-only #{base}..#{patch} -- pmd/core pmd/java"
67
+ diff_cmd = "git diff --name-only #{base}..#{patch} -- pmd-core/src/main pmd-java/src/main"
52
68
  filenames = Cmd.execute(diff_cmd)
53
69
  end
54
70
  filenames.split("\n")
55
71
  end
56
72
 
57
- def get_rule_sets(filenames)
58
- rule_sets = Set[]
73
+ def get_rule_refs(filenames)
74
+ categories, rules = determine_categories_rules(filenames)
75
+ logger.debug "Categories: #{categories}"
76
+ logger.debug "Rules: #{rules}"
77
+
78
+ # filter out all individual rules that are already covered by a complete category
79
+ categories.each do |cat|
80
+ rules.delete_if { |e| e.start_with?(cat) }
81
+ end
82
+
83
+ refs = Set[]
84
+ refs.merge(categories)
85
+ refs.merge(rules)
86
+ refs
87
+ end
88
+
89
+ def determine_categories_rules(filenames)
90
+ categories = Set[]
91
+ rules = Set[]
59
92
  filenames.each do |filename|
60
- match_data = %r{#{PATH_TO_PMD_JAVA_BASED_RULES}/([^/]+)/[^/]+Rule.java}.match(filename)
61
- if match_data.nil?
62
- match_data = %r{#{PATH_TO_PMD_XPATH_BASED_RULES}/([^/]+).xml}.match(filename)
93
+ match_data = check_single_filename(filename)
94
+
95
+ unless match_data.nil?
96
+ if match_data.size == 2
97
+ categories.add("#{match_data[1]}.xml")
98
+ else
99
+ rules.add("#{match_data[1]}.xml/#{match_data[2]}")
100
+ end
63
101
  end
64
102
 
65
- category = if match_data.nil?
66
- nil
67
- else
68
- match_data[1]
69
- end
103
+ next unless match_data.nil?
70
104
 
71
- if category.nil?
72
- rule_sets = ALL_RULE_SETS
73
- break
74
- else
75
- rule_sets.add(category)
76
- end
105
+ logger.debug "Change doesn't match specific rule/category - enable all rules"
106
+ categories = ALL_CATEGORIES
107
+ rules.clear
108
+ break
77
109
  end
78
- rule_sets
110
+ [categories, rules]
79
111
  end
80
112
 
81
- def build_config_file(rule_sets)
82
- if rule_sets.empty?
83
- puts NO_JAVA_RULES_CHANGED_MESSAGE
84
- exit 0
85
- end
113
+ def check_single_filename(filename)
114
+ logger.debug "Checking #{filename}"
115
+ match_data = %r{#{PATH_TO_PMD_JAVA_BASED_RULES}/([^/]+)/([^/]+)Rule.java}.match(filename)
116
+ match_data = %r{#{PATH_TO_PMD_XPATH_BASED_RULES}/([^/]+).xml}.match(filename) if match_data.nil?
117
+ logger.debug "Matches: #{match_data.inspect}"
118
+ match_data
119
+ end
86
120
 
87
- doc = Nokogiri::XML(File.read(PATH_TO_ALL_JAVA_RULES))
88
- doc.search('rule').each do |rule|
89
- rule.remove unless match_ref?(rule, rule_sets)
121
+ def build_config_file(rule_refs)
122
+ if rule_refs.empty?
123
+ logger.info NO_JAVA_RULES_CHANGED_MESSAGE
124
+ return
90
125
  end
91
126
 
92
- description = doc.at_css('description')
93
- description.content = 'The ruleset generated by PmdTester dynamically'
94
-
95
- write_dynamic_file(doc)
96
- end
97
-
98
- def match_ref?(rule_node, rule_sets)
99
- rule_sets.each do |rule_set|
100
- return true unless rule_node['ref'].index(rule_set).nil?
127
+ if rule_refs == ALL_CATEGORIES
128
+ logger.debug 'All rules are used. Not generating a dynamic ruleset.'
129
+ logger.debug "Using the configured/default ruleset base_config=#{@options.base_config} "\
130
+ "patch_config=#{@options.patch_config}"
131
+ return
101
132
  end
102
133
 
103
- false
134
+ write_dynamic_file(rule_refs)
104
135
  end
105
136
 
106
- def write_dynamic_file(doc)
137
+ def write_dynamic_file(rule_refs)
138
+ logger.debug "Generating dynamic configuration for: #{[rule_refs]}"
139
+ builder = Nokogiri::XML::Builder.new(encoding: 'UTF-8') do |xml|
140
+ xml.ruleset('xmlns' => 'http://pmd.sourceforge.net/ruleset/2.0.0',
141
+ 'xmlns:xsi' => 'http://www.w3.org/2001/XMLSchema-instance',
142
+ 'xsi:schemaLocation' => 'http://pmd.sourceforge.net/ruleset/2.0.0 https://pmd.sourceforge.io/ruleset_2_0_0.xsd',
143
+ 'name' => 'Dynamic PmdTester Ruleset') do
144
+ xml.description 'The ruleset generated by PmdTester dynamically'
145
+ rule_refs.each do |entry|
146
+ xml.rule('ref' => "category/java/#{entry}")
147
+ end
148
+ end
149
+ end
150
+ doc = builder.to_xml(indent: 4, encoding: 'UTF-8')
107
151
  File.open(PATH_TO_DYNAMIC_CONFIG, 'w') do |x|
108
- x << doc.to_s.gsub(/\n\s+\n/, "\n")
152
+ x << doc.gsub(/\n\s+\n/, "\n")
109
153
  end
110
154
  @options.base_config = PATH_TO_DYNAMIC_CONFIG
111
155
  @options.patch_config = PATH_TO_DYNAMIC_CONFIG
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rufus-scheduler'
4
+
5
+ module PmdTester
6
+ # Helper class that provides a simple progress logging
7
+ class SimpleProgressLogger
8
+ include PmdTester
9
+ def initialize(task_name)
10
+ @task_name = task_name
11
+ end
12
+
13
+ def start
14
+ logger.info "Starting #{@task_name}"
15
+ message_counter = 1
16
+ @scheduler = Rufus::Scheduler.new
17
+ @scheduler.every '2m' do
18
+ logger.info "Still #{@task_name} (#{message_counter})..."
19
+ message_counter += 1
20
+ end
21
+ end
22
+
23
+ def stop
24
+ @scheduler.shutdown
25
+ end
26
+ end
27
+ end