pmdtester 1.0.0.pre.beta3 → 1.1.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.ci/build.sh +67 -0
- data/.ci/files/env.gpg +1 -0
- data/.ci/inc/install-openjdk.inc +26 -0
- data/.ci/manual-integration-tests.sh +20 -0
- data/.github/workflows/build.yml +39 -0
- data/.github/workflows/manual-integration-tests.yml +32 -0
- data/.gitignore +9 -0
- data/.hoerc +1 -1
- data/.rubocop.yml +13 -2
- data/.rubocop_todo.yml +7 -8
- data/.ruby-version +1 -0
- data/Gemfile +1 -12
- data/History.md +104 -0
- data/Manifest.txt +30 -6
- data/README.rdoc +110 -60
- data/Rakefile +27 -15
- data/config/all-java.xml +1 -1
- data/config/design.xml +1 -1
- data/config/projectlist_1_0_0.xsd +2 -1
- data/config/projectlist_1_1_0.xsd +31 -0
- data/lib/pmdtester.rb +12 -4
- data/lib/pmdtester/builders/liquid_renderer.rb +73 -0
- data/lib/pmdtester/builders/pmd_report_builder.rb +134 -60
- data/lib/pmdtester/builders/project_builder.rb +100 -0
- data/lib/pmdtester/builders/project_hasher.rb +126 -0
- data/lib/pmdtester/builders/rule_set_builder.rb +94 -48
- data/lib/pmdtester/builders/simple_progress_logger.rb +27 -0
- data/lib/pmdtester/builders/summary_report_builder.rb +62 -117
- data/lib/pmdtester/cmd.rb +15 -1
- data/lib/pmdtester/collection_by_file.rb +55 -0
- data/lib/pmdtester/parsers/options.rb +25 -2
- data/lib/pmdtester/parsers/pmd_report_document.rb +79 -27
- data/lib/pmdtester/parsers/projects_parser.rb +2 -4
- data/lib/pmdtester/pmd_branch_detail.rb +36 -12
- data/lib/pmdtester/pmd_configerror.rb +62 -0
- data/lib/pmdtester/pmd_error.rb +34 -34
- data/lib/pmdtester/pmd_report_detail.rb +10 -13
- data/lib/pmdtester/pmd_tester_utils.rb +57 -0
- data/lib/pmdtester/pmd_violation.rb +66 -26
- data/lib/pmdtester/project.rb +28 -23
- data/lib/pmdtester/report_diff.rb +194 -70
- data/lib/pmdtester/resource_locator.rb +4 -0
- data/lib/pmdtester/runner.rb +81 -54
- data/pmdtester.gemspec +64 -0
- data/resources/_includes/diff_pill_row.html +6 -0
- data/resources/css/bootstrap.min.css +7 -0
- data/resources/css/datatables.min.css +36 -0
- data/resources/css/pmd-tester.css +132 -0
- data/resources/js/bootstrap.min.js +7 -0
- data/resources/js/code-snippets.js +73 -0
- data/resources/js/datatables.min.js +726 -0
- data/resources/js/jquery-3.2.1.slim.min.js +4 -0
- data/resources/js/jquery.min.js +2 -0
- data/resources/js/popper.min.js +5 -0
- data/resources/js/project-report.js +136 -0
- data/resources/project_diff_report.html +205 -0
- data/resources/project_index.html +102 -0
- metadata +122 -38
- data/.travis.yml +0 -22
- data/lib/pmdtester/builders/diff_builder.rb +0 -30
- data/lib/pmdtester/builders/diff_report_builder.rb +0 -225
- data/lib/pmdtester/builders/html_report_builder.rb +0 -33
- data/resources/css/maven-base.css +0 -155
- 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
|
-
|
13
|
-
|
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,18 +26,35 @@ module PmdTester
|
|
25
26
|
|
26
27
|
def build
|
27
28
|
filenames = diff_filenames
|
28
|
-
|
29
|
-
output_filter_set(
|
30
|
-
build_config_file(
|
31
|
-
logger.debug "Dynamic configuration: #{[
|
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
|
32
34
|
end
|
33
35
|
|
34
|
-
def output_filter_set(
|
35
|
-
if
|
36
|
-
|
37
|
-
|
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
|
38
55
|
else
|
39
|
-
|
56
|
+
logger.debug "Filter is now #{rule_refs}"
|
57
|
+
@options.filter_set = rule_refs
|
40
58
|
end
|
41
59
|
end
|
42
60
|
|
@@ -46,64 +64,92 @@ module PmdTester
|
|
46
64
|
base = @options.base_branch
|
47
65
|
patch = @options.patch_branch
|
48
66
|
# We only need to support git here, since PMD's repo is using git.
|
49
|
-
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"
|
50
68
|
filenames = Cmd.execute(diff_cmd)
|
51
69
|
end
|
52
70
|
filenames.split("\n")
|
53
71
|
end
|
54
72
|
|
55
|
-
def
|
56
|
-
|
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[]
|
57
92
|
filenames.each do |filename|
|
58
|
-
match_data =
|
59
|
-
|
60
|
-
|
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
|
61
101
|
end
|
62
102
|
|
63
|
-
|
64
|
-
nil
|
65
|
-
else
|
66
|
-
match_data[1]
|
67
|
-
end
|
103
|
+
next unless match_data.nil?
|
68
104
|
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
rule_sets.add(category)
|
74
|
-
end
|
105
|
+
logger.debug "Change doesn't match specific rule/category - enable all rules"
|
106
|
+
categories = ALL_CATEGORIES
|
107
|
+
rules.clear
|
108
|
+
break
|
75
109
|
end
|
76
|
-
|
110
|
+
[categories, rules]
|
111
|
+
end
|
112
|
+
|
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
|
77
119
|
end
|
78
120
|
|
79
|
-
def build_config_file(
|
80
|
-
if
|
121
|
+
def build_config_file(rule_refs)
|
122
|
+
if rule_refs.empty?
|
81
123
|
logger.info NO_JAVA_RULES_CHANGED_MESSAGE
|
82
|
-
|
124
|
+
return
|
83
125
|
end
|
84
126
|
|
85
|
-
|
86
|
-
|
87
|
-
|
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
|
88
132
|
end
|
89
133
|
|
90
|
-
|
91
|
-
description.content = 'The ruleset generated by PmdTester dynamically'
|
92
|
-
|
93
|
-
write_dynamic_file(doc)
|
134
|
+
write_dynamic_file(rule_refs)
|
94
135
|
end
|
95
136
|
|
96
|
-
def
|
97
|
-
|
98
|
-
|
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
|
99
149
|
end
|
100
|
-
|
101
|
-
false
|
102
|
-
end
|
103
|
-
|
104
|
-
def write_dynamic_file(doc)
|
150
|
+
doc = builder.to_xml(indent: 4, encoding: 'UTF-8')
|
105
151
|
File.open(PATH_TO_DYNAMIC_CONFIG, 'w') do |x|
|
106
|
-
x << doc.
|
152
|
+
x << doc.gsub(/\n\s+\n/, "\n")
|
107
153
|
end
|
108
154
|
@options.base_config = PATH_TO_DYNAMIC_CONFIG
|
109
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
|
@@ -2,145 +2,90 @@
|
|
2
2
|
|
3
3
|
module PmdTester
|
4
4
|
# Building summary report to show the details about projects and pmd branchs
|
5
|
-
class SummaryReportBuilder
|
5
|
+
class SummaryReportBuilder
|
6
6
|
include PmdTester
|
7
|
-
|
8
|
-
|
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
|
-
details
|
33
|
-
end
|
7
|
+
include LiquidRenderer
|
8
|
+
include ProjectHasher
|
34
9
|
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
end
|
10
|
+
REPORT_DIR = 'target/reports/diff'
|
11
|
+
BASE_CONFIG_NAME = 'base_config.xml'
|
12
|
+
PATCH_CONFIG_NAME = 'patch_config.xml'
|
39
13
|
|
40
|
-
def
|
41
|
-
|
42
|
-
|
43
|
-
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}")
|
44
17
|
end
|
45
|
-
|
18
|
+
logger.info 'Built all difference reports successfully!'
|
46
19
|
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
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}!"
|
52
25
|
end
|
53
26
|
|
54
|
-
|
55
|
-
doc.thead do
|
56
|
-
doc.tr do
|
57
|
-
doc.th 'Item'
|
58
|
-
doc.th 'base'
|
59
|
-
doc.th 'patch'
|
60
|
-
end
|
61
|
-
end
|
62
|
-
end
|
27
|
+
private
|
63
28
|
|
64
|
-
def
|
65
|
-
|
66
|
-
|
67
|
-
@patch_details.branch_name)
|
68
|
-
build_branch_table_row(doc, 'branch last commit sha', @base_details.branch_last_sha,
|
69
|
-
@patch_details.branch_last_sha)
|
70
|
-
build_branch_table_row(doc, 'branch last commit message', @base_details.branch_last_message,
|
71
|
-
@patch_details.branch_last_message)
|
72
|
-
build_branch_table_row(doc, 'total execution time', @base_details.format_execution_time,
|
73
|
-
@patch_details.format_execution_time)
|
74
|
-
build_branch_config_table_row(doc)
|
75
|
-
end
|
29
|
+
def process_project(project, dir)
|
30
|
+
logger.info "Rendering #{project.name}..."
|
31
|
+
LiquidProjectRenderer.new.write_project_index(project, dir)
|
76
32
|
end
|
77
33
|
|
78
|
-
def
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
copy_branch_config_file(base_config_src_path, BASE_CONFIG_PATH)
|
83
|
-
doc.td(class: 'a') do
|
84
|
-
doc.a(href: './base_config.xml') { doc.text 'base config' }
|
85
|
-
end
|
86
|
-
patch_config_stc_path = @patch_details.target_branch_config_path
|
87
|
-
FileUtils.cp(patch_config_stc_path, PATCH_CONFIG_PATH)
|
88
|
-
doc.td(class: 'b') do
|
89
|
-
doc.a(href: './patch_config.xml') { doc.text 'patch config' }
|
90
|
-
end
|
91
|
-
end
|
34
|
+
def write_structure(target_root)
|
35
|
+
logger.info 'Copying resources...'
|
36
|
+
copy_resource('css', target_root)
|
37
|
+
copy_resource('js', target_root)
|
92
38
|
end
|
93
39
|
|
94
|
-
def
|
95
|
-
|
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}")
|
96
43
|
end
|
97
44
|
|
98
|
-
def
|
99
|
-
|
100
|
-
doc.td(class: 'c') { doc.text item }
|
101
|
-
doc.td(class: 'a') { doc.text base }
|
102
|
-
doc.td(class: 'b') { doc.text patch }
|
103
|
-
end
|
45
|
+
def copy_file(src, dest)
|
46
|
+
FileUtils.cp(src, dest) if File.exist?(src)
|
104
47
|
end
|
105
48
|
|
106
|
-
def
|
107
|
-
|
108
|
-
|
109
|
-
|
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
|
+
}
|
110
57
|
end
|
111
|
-
end
|
112
58
|
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
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)
|
118
67
|
end
|
119
68
|
|
120
|
-
def
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
doc.th 'project branch/tag'
|
125
|
-
doc.th 'diff exist?'
|
126
|
-
doc.th 'introduce new errors?'
|
127
|
-
end
|
128
|
-
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}"
|
129
73
|
end
|
130
74
|
|
131
|
-
def
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
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
|
+
'execution_time' => PmdReportDetail.convert_seconds(details.execution_time),
|
84
|
+
'jdk_info' => details.jdk_version,
|
85
|
+
'locale' => details.language,
|
86
|
+
'config_url' => config_name,
|
87
|
+
'pr_number' => details.pull_request
|
88
|
+
}
|
144
89
|
end
|
145
90
|
end
|
146
91
|
end
|
data/lib/pmdtester/cmd.rb
CHANGED
@@ -7,20 +7,34 @@ module PmdTester
|
|
7
7
|
class Cmd
|
8
8
|
extend PmdTester
|
9
9
|
def self.execute(cmd)
|
10
|
+
stdout, _stderr, _status = internal_execute(cmd)
|
11
|
+
stdout&.chomp!
|
12
|
+
stdout
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.stderr_of(cmd)
|
16
|
+
_stdout, stderr, _status = internal_execute(cmd)
|
17
|
+
stderr
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.internal_execute(cmd)
|
10
21
|
logger.debug "execute command '#{cmd}'"
|
11
22
|
|
12
23
|
stdout, stderr, status = Open3.capture3("#{cmd};")
|
13
24
|
|
14
25
|
logger.debug stdout
|
15
26
|
unless status.success?
|
27
|
+
logger.error stdout
|
16
28
|
logger.error stderr
|
17
29
|
raise CmdException.new(cmd, stderr)
|
18
30
|
end
|
19
31
|
|
20
32
|
stdout&.chomp!
|
21
33
|
|
22
|
-
stdout
|
34
|
+
[stdout, stderr, status]
|
23
35
|
end
|
36
|
+
|
37
|
+
private_class_method :internal_execute
|
24
38
|
end
|
25
39
|
|
26
40
|
# The exception should be raised when the shell command failed.
|