pmdtester 1.1.2 → 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.
@@ -0,0 +1,39 @@
1
+ <?xml version="1.0" ?>
2
+ <!-- version 1.1.0 -->
3
+ <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
4
+ <xs:element name="projectlist">
5
+ <xs:complexType>
6
+ <xs:sequence>
7
+ <xs:element name="description" type="xs:string" minOccurs="1" maxOccurs="1"/>
8
+ <xs:element name="project" type="project" minOccurs="1" maxOccurs="unbounded"/>
9
+ </xs:sequence>
10
+ </xs:complexType>
11
+ </xs:element>
12
+ <xs:complexType name="project">
13
+ <xs:sequence>
14
+ <xs:element name="name" type="xs:string" minOccurs="1" maxOccurs="1"/>
15
+ <xs:element name="type" minOccurs="1" maxOccurs="1">
16
+ <xs:simpleType>
17
+ <xs:restriction base="xs:string">
18
+ <xs:enumeration value="git"/>
19
+ </xs:restriction>
20
+ </xs:simpleType>
21
+ </xs:element>
22
+ <xs:element name="connection" type="xs:string" minOccurs="1" maxOccurs="1"/>
23
+ <xs:element name="webview-url" type="xs:string" minOccurs="0" maxOccurs="1"/>
24
+ <xs:element name="tag" type="xs:string" minOccurs="0" maxOccurs="1"/>
25
+ <xs:element name="src-subpath" type="xs:string" minOccurs="0" maxOccurs="1" default=".">
26
+ <xs:annotation>
27
+ <xs:documentation>
28
+ Value of the -dir option for the PMD run.
29
+ The value must be a directory path relative to the root directory of the clone.
30
+ Defaults to just '.', ie the entire directory.
31
+ </xs:documentation>
32
+ </xs:annotation>
33
+ </xs:element>
34
+ <xs:element name="exclude-pattern" type="xs:string" minOccurs="0" maxOccurs="unbounded"/>
35
+ <xs:element name="build-command" type="xs:string" minOccurs="0" maxOccurs="1"/>
36
+ <xs:element name="auxclasspath-command" type="xs:string" minOccurs="0" maxOccurs="1"/>
37
+ </xs:sequence>
38
+ </xs:complexType>
39
+ </xs:schema>
data/lib/pmdtester.rb CHANGED
@@ -32,7 +32,7 @@ require_relative 'pmdtester/parsers/projects_parser'
32
32
  # and unexpected behaviors will not be introduced to PMD project
33
33
  # after fixing an issue and new rules can work as expected.
34
34
  module PmdTester
35
- VERSION = '1.1.2'
35
+ VERSION = '1.2.0'
36
36
  BASE = 'base'
37
37
  PATCH = 'patch'
38
38
  PR_NUM_ENV_VAR = 'PMD_CI_PULL_REQUEST_NUMBER' # see PmdBranchDetail
@@ -57,17 +57,74 @@ module PmdTester
57
57
  write_file("#{root}/index.html", render_liquid('project_diff_report.html', liquid_env))
58
58
  # generate array of violations in json
59
59
  write_file("#{root}/project_data.js", dump_violations_json(project))
60
+ # copy original pmd reports
61
+ copy_file("#{root}/base_pmd_report.xml", project.report_diff.base_report.file)
62
+ copy_file("#{root}/patch_pmd_report.xml", project.report_diff.patch_report.file)
63
+ # render full pmd reports
64
+ write_file("#{root}/base_pmd_report.html",
65
+ render_liquid('project_pmd_report.html', pmd_report_liquid_env(project, BASE)))
66
+ write_file("#{root}/base_data.js", dump_violations_json(project, BASE))
67
+ write_file("#{root}/patch_pmd_report.html",
68
+ render_liquid('project_pmd_report.html', pmd_report_liquid_env(project, PATCH)))
69
+ write_file("#{root}/patch_data.js", dump_violations_json(project, PATCH))
60
70
  end
61
71
 
62
- def dump_violations_json(project)
72
+ def dump_violations_json(project, branch = 'diff')
73
+ violations_by_file = if branch == BASE
74
+ project.report_diff.base_report.violations_by_file.to_h
75
+ elsif branch == PATCH
76
+ project.report_diff.patch_report.violations_by_file.to_h
77
+ else
78
+ project.report_diff.violation_diffs_by_file
79
+ end
80
+
63
81
  h = {
64
82
  'source_link_base' => project.webview_url,
65
83
  'source_link_template' => link_template(project),
66
- **violations_to_hash(project)
84
+ **violations_to_hash(project, violations_by_file, branch == 'diff')
67
85
  }
68
86
 
69
- project_data = JSON.fast_generate(h)
87
+ project_data = JSON.fast_generate(h, indent: ' ', object_nl: "\n", array_nl: "\n")
70
88
  "let project = #{project_data}"
71
89
  end
90
+
91
+ private
92
+
93
+ def copy_file(target_file, source_file)
94
+ if File.exist? source_file
95
+ FileUtils.cp(source_file, target_file)
96
+ logger&.info "Written #{target_file}"
97
+ else
98
+ logger&.warn "File #{source_file} not found"
99
+ end
100
+ end
101
+
102
+ def pmd_report_liquid_env(project, branch)
103
+ report = if branch == BASE
104
+ project.report_diff.base_report
105
+ else
106
+ project.report_diff.patch_report
107
+ end
108
+ {
109
+ 'project_name' => project.name,
110
+ 'branch' => branch,
111
+ 'report' => report_to_h(project, report)
112
+ }
113
+ end
114
+
115
+ def report_to_h(project, report)
116
+ {
117
+ 'violation_counts' => report.violations_by_file.total_size,
118
+ 'error_counts' => report.errors_by_file.total_size,
119
+ 'configerror_counts' => report.configerrors_by_rule.values.flatten.length,
120
+
121
+ 'execution_time' => PmdReportDetail.convert_seconds(report.exec_time),
122
+ 'timestamp' => report.timestamp,
123
+
124
+ 'rules' => report.rule_summaries,
125
+ 'errors' => report.errors_by_file.all_values.map { |e| error_to_hash(e, project) },
126
+ 'configerrors' => report.configerrors_by_rule.values.flatten.map { |e| configerror_to_hash(e) }
127
+ }
128
+ end
72
129
  end
73
130
  end
@@ -69,7 +69,7 @@ module PmdTester
69
69
  ' -Dmaven.source.skip=true' \
70
70
  ' -Dcheckstyle.skip=true' \
71
71
  ' -Dpmd.skip=true' \
72
- ' -T1C'
72
+ ' -T1C -B'
73
73
  Cmd.execute(package_cmd)
74
74
  end
75
75
 
@@ -119,7 +119,7 @@ module PmdTester
119
119
  doc = Nokogiri::XML(File.read(@branch_config))
120
120
  ruleset = doc.at_css('ruleset')
121
121
  ruleset.add_child("\n")
122
- project.exclude_pattern.each do |exclude_pattern|
122
+ project.exclude_patterns.each do |exclude_pattern|
123
123
  ruleset.add_child(" <exclude-pattern>#{exclude_pattern}</exclude-pattern>\n")
124
124
  end
125
125
 
@@ -17,11 +17,19 @@ module PmdTester
17
17
 
18
18
  @projects.each do |project|
19
19
  logger.info "Start cloning #{project.name} repository"
20
- path = project.local_source_path
21
- clone_cmd = "#{project.type} clone #{project.connection} #{path}"
20
+ path = project.clone_root_path
21
+
22
22
  if File.exist?(path)
23
23
  logger.warn "Skipping clone, project path #{path} already exists"
24
24
  else
25
+ raise "Unsupported project type '#{project.type}' - only git is supported" unless project.type == 'git'
26
+
27
+ # git:
28
+ # Don't download whole history
29
+ # Note we don't use --single-branch, because the repo is downloaded
30
+ # once but may be used with several tags.
31
+ clone_cmd = "git clone --no-single-branch --depth 1 #{project.connection} #{path}"
32
+
25
33
  Cmd.execute(clone_cmd)
26
34
  end
27
35
 
@@ -38,7 +46,7 @@ module PmdTester
38
46
  logger.info 'Building projects started'
39
47
 
40
48
  @projects.each do |project|
41
- path = project.local_source_path
49
+ path = project.clone_root_path
42
50
  Dir.chdir(path) do
43
51
  progress_logger = SimpleProgressLogger.new("building #{project.name} in #{path}")
44
52
  progress_logger.start
@@ -87,12 +95,9 @@ module PmdTester
87
95
  end
88
96
 
89
97
  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
98
+ raise "Unsupported project type '#{type}' - only git is supported" unless type == 'git'
99
+
100
+ reset_cmd = "git checkout #{tag}; git reset --hard #{tag}"
96
101
 
97
102
  Cmd.execute(reset_cmd)
98
103
  end
@@ -25,24 +25,14 @@ module PmdTester
25
25
  }
26
26
  end
27
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)
28
+ def violations_to_hash(project, violations_by_file, is_diff)
39
29
  filename_index = []
40
30
  all_vs = []
41
- project.report_diff.violation_diffs_by_file.each do |file, vs|
31
+ violations_by_file.each do |file, vs|
42
32
  file_ref = filename_index.size
43
33
  filename_index.push(project.get_local_path(file))
44
34
  vs.each do |v|
45
- all_vs.push(make_violation_hash(file_ref, v))
35
+ all_vs.push(make_violation_hash(file_ref, v, is_diff))
46
36
  end
47
37
  end
48
38
 
@@ -52,36 +42,19 @@ module PmdTester
52
42
  }
53
43
  end
54
44
 
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
45
+ def errors_to_h(project)
46
+ errors = project.report_diff.error_diffs_by_file.values.flatten
47
+ errors.map { |e| error_to_hash(e, project) }
68
48
  end
69
49
 
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
50
+ def configerrors_to_h(project)
51
+ configerrors = project.report_diff.configerror_diffs_by_rule.values.flatten
52
+ configerrors.map { |e| configerror_to_hash(e) }
80
53
  end
81
54
 
82
- def diff_fragments(violation)
83
- diff = Differ.diff_by_word(violation.message, violation.old_message)
84
- diff.format_as(:html)
55
+ def link_template(project)
56
+ l_str = project.type == 'git' ? 'L' : 'l'
57
+ "#{project.webview_url}/{file}##{l_str}{line}"
85
58
  end
86
59
 
87
60
  def error_to_hash(error, project)
@@ -122,5 +95,34 @@ module PmdTester
122
95
  'added'
123
96
  end
124
97
  end
98
+
99
+ private
100
+
101
+ def violation_type(violation)
102
+ if violation.changed?
103
+ '~'
104
+ elsif violation.branch == PATCH
105
+ '+'
106
+ else
107
+ '-'
108
+ end
109
+ end
110
+
111
+ def make_violation_hash(file_ref, violation, is_diff = TRUE)
112
+ h = {
113
+ 't' => is_diff ? violation_type(violation) : '+',
114
+ 'l' => violation.line,
115
+ 'f' => file_ref,
116
+ 'r' => violation.rule_name,
117
+ 'm' => is_diff && violation.changed? ? diff_fragments(violation) : violation.message
118
+ }
119
+ h['ol'] = violation.old_line if is_diff && violation.changed? && violation.line != violation.old_line
120
+ h
121
+ end
122
+
123
+ def diff_fragments(violation)
124
+ diff = Differ.diff_by_word(violation.message, violation.old_message)
125
+ diff.format_as(:html)
126
+ end
125
127
  end
126
128
  end
@@ -33,6 +33,10 @@ module PmdTester
33
33
  rule_refs
34
34
  end
35
35
 
36
+ def calculate_filter_set
37
+ output_filter_set(ALL_CATEGORIES)
38
+ end
39
+
36
40
  def output_filter_set(rule_refs)
37
41
  if rule_refs == ALL_CATEGORIES
38
42
  if @options.mode == Options::ONLINE
@@ -80,6 +80,7 @@ module PmdTester
80
80
  'sha' => details.branch_last_sha,
81
81
  'message' => details.branch_last_message
82
82
  },
83
+ 'timestamp' => details.timestamp,
83
84
  'execution_time' => PmdReportDetail.convert_seconds(details.execution_time),
84
85
  'jdk_info' => details.jdk_version,
85
86
  'locale' => details.language,
@@ -29,6 +29,7 @@ module PmdTester
29
29
  attr_reader :threads
30
30
  attr_reader :html_flag
31
31
  attr_reader :auto_config_flag
32
+ attr_reader :filter_with_patch_config
32
33
  attr_reader :debug_flag
33
34
  attr_accessor :filter_set
34
35
  attr_reader :keep_reports
@@ -48,6 +49,7 @@ module PmdTester
48
49
  @threads = options[:t]
49
50
  @html_flag = options[:f]
50
51
  @auto_config_flag = options[:a]
52
+ @filter_with_patch_config = options.filter_with_patch_config?
51
53
  @debug_flag = options[:d]
52
54
  @filter_set = nil
53
55
  @keep_reports = options.keep_reports?
@@ -100,6 +102,9 @@ module PmdTester
100
102
  o.bool '-a', '--auto-gen-config',
101
103
  'whether to generate configurations automatically based on branch differences,' \
102
104
  'this option only works in online and local mode'
105
+ o.bool '--filter-with-patch-config',
106
+ 'whether to use patch config to filter baseline result as if --auto-gen-config ' \
107
+ 'has been used. This option only works in online mode.'
103
108
  o.bool '--keep-reports',
104
109
  'whether to keep old reports and skip running PMD again if possible'
105
110
  o.bool '-d', '--debug',
@@ -8,14 +8,12 @@ module PmdTester
8
8
  attr_reader :violations
9
9
  attr_reader :errors
10
10
  attr_reader :configerrors
11
- attr_reader :infos_by_rules
12
11
 
13
12
  def initialize(branch_name, working_dir, filter_set = nil)
14
13
  @violations = CollectionByFile.new
15
14
  @errors = CollectionByFile.new
16
15
  @configerrors = Hash.new { |hash, key| hash[key] = [] }
17
16
 
18
- @infos_by_rules = {}
19
17
  @current_violations = []
20
18
  @current_violation = nil
21
19
  @current_error = nil
@@ -21,7 +21,7 @@ module PmdTester
21
21
  end
22
22
 
23
23
  def schema_file_path
24
- ResourceLocator.locate('config/projectlist_1_1_0.xsd')
24
+ ResourceLocator.locate('config/projectlist_1_2_0.xsd')
25
25
  end
26
26
  end
27
27
 
@@ -10,6 +10,8 @@ module PmdTester
10
10
  attr_accessor :branch_last_sha
11
11
  attr_accessor :branch_last_message
12
12
  attr_accessor :branch_name
13
+ # Start of the regression report
14
+ attr_accessor :timestamp
13
15
  # The branch's execution time on all standard projects
14
16
  attr_accessor :execution_time
15
17
  attr_accessor :jdk_version
@@ -26,6 +28,7 @@ module PmdTester
26
28
  @branch_name = branch_name
27
29
  branch_filename = PmdBranchDetail.branch_filename(branch_name)
28
30
  @base_branch_dir = "target/reports/#{branch_filename}" unless @branch_name.nil?
31
+ @timestamp = Time.now
29
32
  @execution_time = 0
30
33
  # the result of command 'java -version' is going to stderr
31
34
  @jdk_version = Cmd.stderr_of('java -version')
@@ -42,11 +45,13 @@ module PmdTester
42
45
  details.branch_last_sha = hash['branch_last_sha']
43
46
  details.branch_last_message = hash['branch_last_message']
44
47
  details.branch_name = hash['branch_name']
48
+ details.timestamp = hash['timestamp']
45
49
  details.execution_time = hash['execution_time']
46
50
  details.jdk_version = hash['jdk_version']
47
51
  details.language = hash['language']
48
52
  details.pull_request = hash['pull_request']
49
53
  else
54
+ details.timestamp = Time.now
50
55
  details.jdk_version = ''
51
56
  details.language = ''
52
57
  logger&.warn "#{details.path_to_save_file} doesn't exist!"
@@ -58,6 +63,7 @@ module PmdTester
58
63
  hash = { branch_last_sha: @branch_last_sha,
59
64
  branch_last_message: @branch_last_message,
60
65
  branch_name: @branch_name,
66
+ timestamp: @timestamp,
61
67
  execution_time: @execution_time,
62
68
  jdk_version: @jdk_version,
63
69
  language: @language,
@@ -29,6 +29,7 @@ module PmdTester
29
29
  parser.parse_file(report_file) if File.exist?(report_file)
30
30
  Report.new(
31
31
  report_document: doc,
32
+ file: report_file,
32
33
 
33
34
  timestamp: report_details.timestamp,
34
35
  exec_time: report_details.execution_time
@@ -1,5 +1,7 @@
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
@@ -12,7 +14,8 @@ module PmdTester
12
14
  attr_reader :connection
13
15
  attr_reader :webview_url
14
16
  attr_reader :tag
15
- attr_reader :exclude_pattern
17
+ attr_reader :exclude_patterns
18
+ attr_reader :src_subpath
16
19
  attr_accessor :report_diff
17
20
  # key: pmd branch name as String => value: local path of pmd report
18
21
  attr_reader :build_command
@@ -20,7 +23,7 @@ module PmdTester
20
23
  # stores the auxclasspath calculated after cloning/preparing the project
21
24
  attr_accessor :auxclasspath
22
25
 
23
- def initialize(project)
26
+ def initialize(project) # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
24
27
  @name = project.at_xpath('name').text
25
28
  @type = project.at_xpath('type').text
26
29
  @connection = project.at_xpath('connection').text
@@ -31,9 +34,10 @@ module PmdTester
31
34
  @webview_url = default_webview_url
32
35
  @webview_url = webview_url_element.text unless webview_url_element.nil?
33
36
 
34
- @exclude_pattern = []
37
+ @src_subpath = project.at_xpath('src-subpath')&.text || '.'
38
+ @exclude_patterns = []
35
39
  project.xpath('exclude-pattern').each do |ep|
36
- @exclude_pattern.push(ep.text)
40
+ @exclude_patterns.push(ep.text)
37
41
  end
38
42
 
39
43
  @build_command = project.at_xpath('build-command')&.text
@@ -56,17 +60,17 @@ module PmdTester
56
60
  # Change the file path from 'LOCAL_DIR/SOURCE_CODE_PATH' to
57
61
  # 'WEB_VIEW_URL/SOURCE_CODE_PATH'
58
62
  def get_webview_url(file_path)
59
- file_path.gsub(%r{/#{local_source_path}}, @webview_url)
63
+ file_path.gsub(%r{/#{clone_root_path}}, @webview_url)
60
64
  end
61
65
 
62
66
  # Change the file path from 'LOCAL_DIR/SOURCE_CODE_PATH' to
63
67
  # 'PROJECT_NAME/SOURCE_CODE_PATH'
64
68
  def get_path_inside_project(file_path)
65
- file_path.gsub(%r{/#{local_source_path}}, @name)
69
+ file_path.gsub(%r{/#{clone_root_path}}, @name)
66
70
  end
67
71
 
68
72
  def get_local_path(file_path)
69
- file_path.sub(%r{/#{local_source_path}/}, '')
73
+ file_path.sub(%r{/#{clone_root_path}/}, '')
70
74
  end
71
75
 
72
76
  def get_pmd_report_path(branch_name)
@@ -93,6 +97,19 @@ module PmdTester
93
97
  end
94
98
  end
95
99
 
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
105
+ end
106
+
107
+ ##
108
+ # Path to the clone directory
109
+ def clone_root_path
110
+ "#{REPOSITORIES_PATH}/#{@name}"
111
+ end
112
+
96
113
  def get_project_target_dir(branch_name)
97
114
  branch_filename = PmdBranchDetail.branch_filename(branch_name)
98
115
  dir = "target/reports/#{branch_filename}/#{@name}"
@@ -100,10 +117,6 @@ module PmdTester
100
117
  dir
101
118
  end
102
119
 
103
- def local_source_path
104
- "#{REPOSITORIES_PATH}/#{@name}"
105
- end
106
-
107
120
  def compute_report_diff(base_branch, patch_branch, filter_set)
108
121
  self.report_diff = build_report_diff(get_pmd_report_path(base_branch),
109
122
  get_pmd_report_path(patch_branch),