Skip to content

Commit b3f3245

Browse files
committed
Merge pull request #103 from adangel:auto-gen-config-all
Support other languages besides java #103
2 parents 4082a41 + 3b4479b commit b3f3245

File tree

15 files changed

+399
-111
lines changed

15 files changed

+399
-111
lines changed

History.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
## Enhancements
66

7+
* [#103](https://github.com/pmd/pmd-regression-tester/pull/103): Support other languages besides java
8+
79
## Fixed Issues
810

911
## External Contributions

Rakefile

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,18 +19,23 @@ hoe = Hoe.spec 'pmdtester' do
1919
developer 'Clément Fournier', '[email protected]'
2020

2121
self.clean_globs = %w[target/reports/**/* target/test/**/* target/dynamic-config.xml Gemfile.lock]
22-
self.extra_deps += [['nokogiri', '>= 1.11.0.rc4'], ['slop', '~> 4.6'], ['differ', '~> 0.1'],
23-
['rufus-scheduler', '~> 3.5'], ['logger-colors', '~> 1.0'],
24-
['liquid', '>= 4.0']]
22+
self.extra_deps += [
23+
['nokogiri', '~> 1.13'],
24+
['slop', '~> 4.6'],
25+
['differ', '~> 0.1'],
26+
['rufus-scheduler', '~> 3.8'],
27+
['logger-colors', '~> 1.0'],
28+
['liquid', '~> 5.2']
29+
]
2530
self.extra_dev_deps += [
2631
['hoe-bundler', '~> 1.5'],
2732
['hoe-git', '~> 1.6'],
2833
['minitest', '~> 5.10'],
2934
['mocha', '~> 1.5'],
3035
# use the same version of rubocop as codacy
31-
['rubocop', '~> 0.81'],
32-
['test-unit', '~> 3.2'],
33-
['rdoc', ['>= 4.0', '< 7']]
36+
['rubocop', '~> 0.93'],
37+
['test-unit', '~> 3.5'],
38+
['rdoc', '~> 6.4']
3439
]
3540
spec_extras[:required_ruby_version] = '>= 2.7'
3641

lib/pmdtester/builders/rule_set_builder.rb

Lines changed: 113 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -9,73 +9,70 @@ module PmdTester
99
# Attention: we only consider java rulesets now.
1010
class RuleSetBuilder
1111
include PmdTester
12-
ALL_CATEGORIES = Set['bestpractices.xml', 'codestyle.xml', 'design.xml', 'documentation.xml',
13-
'errorprone.xml', 'multithreading.xml', 'performance.xml',
14-
'security.xml'].freeze
15-
PATH_TO_PMD_JAVA_BASED_RULES =
16-
'pmd-java/src/main/java/net/sourceforge/pmd/lang/java/rule'
17-
PATH_TO_PMD_XPATH_BASED_RULES = 'pmd-java/src/main/resources/category/java'
18-
PATH_TO_ALL_JAVA_RULES =
19-
ResourceLocator.locate('config/all-java.xml')
2012
PATH_TO_DYNAMIC_CONFIG = 'target/dynamic-config.xml'
21-
NO_JAVA_RULES_CHANGED_MESSAGE = 'No java rules have been changed!'
13+
NO_RULES_CHANGED_MESSAGE = 'No regression tested rules have been changed!'
2214

2315
def initialize(options)
2416
@options = options
2517
end
2618

27-
def build
28-
filenames = diff_filenames
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
19+
#
20+
# Creates a dynamic ruleset based on the changed sources.
21+
# Returns true, when rules are affected by the changed sources.
22+
# Returns false, when no rules are affected and regression tester can be skipped.
23+
#
24+
def build?
25+
languages = determine_languages
26+
filenames = diff_filenames(languages)
27+
run_required, rule_refs = get_rule_refs(filenames)
28+
if run_required
29+
output_filter_set(rule_refs)
30+
build_config_file(rule_refs)
31+
logger.debug "Dynamic configuration: #{[rule_refs]}"
32+
else
33+
logger.info NO_RULES_CHANGED_MESSAGE
34+
end
35+
run_required
3436
end
3537

3638
def calculate_filter_set
37-
output_filter_set(ALL_CATEGORIES)
39+
output_filter_set([])
3840
end
3941

4042
def output_filter_set(rule_refs)
41-
if rule_refs == ALL_CATEGORIES
42-
if @options.mode == Options::ONLINE
43+
if rule_refs.empty?
44+
if @options.mode == Options::ONLINE && @options.filter_with_patch_config
4345
@options.filter_set = Set[]
4446
doc = File.open(@options.patch_config) { |f| Nokogiri::XML(f) }
4547
rules = doc.css('ruleset rule')
4648
rules.each do |r|
4749
ref = r.attributes['ref'].content
48-
ref.delete_prefix!('category/java/')
50+
ref.delete_prefix!('category/')
4951
@options.filter_set.add(ref)
5052
end
5153

52-
logger.debug "Using filter based on patch config #{@options.patch_config}: " \
54+
logger.info "Using filter based on patch config #{@options.patch_config}: " \
5355
"#{@options.filter_set}"
5456
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+
# if `rule_refs` is empty, then no filter can be used when comparing to the baseline
58+
logger.info 'No filter when comparing patch to baseline'
5759
@options.filter_set = nil
5860
end
5961
else
60-
logger.debug "Filter is now #{rule_refs}"
62+
logger.info "Filter is now #{rule_refs}"
6163
@options.filter_set = rule_refs
6264
end
6365
end
6466

65-
def diff_filenames
66-
filenames = nil
67-
Dir.chdir(@options.local_git_repo) do
68-
base = @options.base_branch
69-
patch = @options.patch_branch
70-
# We only need to support git here, since PMD's repo is using git.
71-
diff_cmd = "git diff --name-only #{base}..#{patch} -- pmd-core/src/main pmd-java/src/main"
72-
filenames = Cmd.execute(diff_cmd)
73-
end
74-
filenames.split("\n")
75-
end
76-
67+
#
68+
# Determines the rules or category rulesets, that are potentially affected by the change.
69+
# Returns an empty set, if all rules are affected and there is no
70+
# filtering possible or if no rules are affected.
71+
# Whether to run the regression test is returned as an additional boolean flag.
72+
#
7773
def get_rule_refs(filenames)
78-
categories, rules = determine_categories_rules(filenames)
74+
run_required, categories, rules = determine_categories_rules(filenames)
75+
logger.debug "Regression test required: #{run_required}"
7976
logger.debug "Categories: #{categories}"
8077
logger.debug "Rules: #{rules}"
8178

@@ -87,48 +84,11 @@ def get_rule_refs(filenames)
8784
refs = Set[]
8885
refs.merge(categories)
8986
refs.merge(rules)
90-
refs
91-
end
92-
93-
def determine_categories_rules(filenames)
94-
categories = Set[]
95-
rules = Set[]
96-
filenames.each do |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
105-
end
106-
107-
next unless match_data.nil?
108-
109-
logger.debug "Change doesn't match specific rule/category - enable all rules"
110-
categories = ALL_CATEGORIES
111-
rules.clear
112-
break
113-
end
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
87+
[run_required, refs]
12388
end
12489

12590
def build_config_file(rule_refs)
12691
if rule_refs.empty?
127-
logger.info NO_JAVA_RULES_CHANGED_MESSAGE
128-
return
129-
end
130-
131-
if rule_refs == ALL_CATEGORIES
13292
logger.debug 'All rules are used. Not generating a dynamic ruleset.'
13393
logger.debug "Using the configured/default ruleset base_config=#{@options.base_config} "\
13494
"patch_config=#{@options.patch_config}"
@@ -147,7 +107,7 @@ def write_dynamic_file(rule_refs)
147107
'name' => 'Dynamic PmdTester Ruleset') do
148108
xml.description 'The ruleset generated by PmdTester dynamically'
149109
rule_refs.each do |entry|
150-
xml.rule('ref' => "category/java/#{entry}")
110+
xml.rule('ref' => "category/#{entry}")
151111
end
152112
end
153113
end
@@ -158,5 +118,82 @@ def write_dynamic_file(rule_refs)
158118
@options.base_config = PATH_TO_DYNAMIC_CONFIG
159119
@options.patch_config = PATH_TO_DYNAMIC_CONFIG
160120
end
121+
122+
private
123+
124+
def determine_categories_rules(filenames)
125+
regression_test_required = false
126+
categories = Set[]
127+
rules = Set[]
128+
filenames.each do |filename|
129+
matched = check_single_filename(filename, categories, rules)
130+
regression_test_required = true if matched
131+
132+
next if matched
133+
134+
logger.info "Change in file #{filename} doesn't match specific rule/category - enable all rules"
135+
regression_test_required = true
136+
categories.clear
137+
rules.clear
138+
break
139+
end
140+
[regression_test_required, categories, rules]
141+
end
142+
143+
def check_single_filename(filename, categories, rules)
144+
logger.debug "Checking #{filename}"
145+
146+
# matches Java-based rule implementations
147+
match_data = %r{.+/src/main/java/.+/lang/([^/]+)/rule/([^/]+)/([^/]+)Rule.java}.match(filename)
148+
unless match_data.nil?
149+
logger.debug "Matches: #{match_data.inspect}"
150+
rules.add("#{match_data[1]}/#{match_data[2]}.xml/#{match_data[3]}")
151+
return true
152+
end
153+
154+
# matches xpath rules
155+
match_data = %r{.+/src/main/resources/category/([^/]+)/([^/]+).xml}.match(filename)
156+
unless match_data.nil?
157+
logger.debug "Matches: #{match_data.inspect}"
158+
categories.add("#{match_data[1]}/#{match_data[2]}.xml")
159+
return true
160+
end
161+
162+
false
163+
end
164+
165+
def diff_filenames(languages)
166+
filenames = nil
167+
Dir.chdir(@options.local_git_repo) do
168+
base = @options.base_branch
169+
patch = @options.patch_branch
170+
171+
filepath_filter = ''
172+
unless languages.empty?
173+
filepath_filter = '-- pmd-core/src/main'
174+
languages.each { |l| filepath_filter = "#{filepath_filter} pmd-#{l}/src/main" }
175+
end
176+
177+
# We only need to support git here, since PMD's repo is using git.
178+
diff_cmd = "git diff --name-only #{base}..#{patch} #{filepath_filter}"
179+
filenames = Cmd.execute(diff_cmd)
180+
end
181+
filenames.split("\n")
182+
end
183+
184+
#
185+
# Determines all languages, that are part of the regression test.
186+
# This is based on the configured rules/rulesets.
187+
#
188+
def determine_languages
189+
languages = Set[]
190+
doc = File.open(@options.patch_config) { |f| Nokogiri::XML(f) }
191+
rules = doc.css('ruleset rule')
192+
rules.each do |r|
193+
ref = r.attributes['ref'].content
194+
languages.add(ref.split('/')[1])
195+
end
196+
languages
197+
end
161198
end
162199
end

lib/pmdtester/parsers/pmd_report_document.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ def match_filter_set?(violation)
9292
ruleset_attr = violation.ruleset_name.delete(' ').downcase! << '.xml'
9393
return true if @filter_set.include?(ruleset_attr)
9494

95-
rule_ref = "#{ruleset_attr}/#{violation.rule_name}"
95+
rule_ref = "#{violation.language}/#{ruleset_attr}/#{violation.rule_name}"
9696

9797
@filter_set.include?(rule_ref)
9898
end

lib/pmdtester/pmd_violation.rb

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ class PmdViolation
2727
# </xs:simpleContent>
2828
# </xs:complexType>
2929

30-
attr_reader :fname, :info_url, :line, :old_line, :old_message, :rule_name, :ruleset_name
30+
attr_reader :fname, :info_url, :line, :old_line, :old_message, :rule_name, :ruleset_name, :language
3131
attr_accessor :message
3232

3333
# rubocop:disable Metrics/ParameterLists
@@ -43,6 +43,8 @@ def initialize(branch:, fname:, info_url:, bline:, rule_name:, ruleset_name:)
4343

4444
@ruleset_name = ruleset_name
4545

46+
@language = determine_language_from_info_url
47+
4648
@changed = false
4749
@old_message = nil
4850
@old_line = nil
@@ -102,5 +104,13 @@ def to_liquid
102104
'changed' => changed?
103105
}
104106
end
107+
108+
private
109+
110+
def determine_language_from_info_url
111+
# @info_url is e.g. http://pmd.sourceforge.net/snapshot/pmd_rules_java_codestyle.html#fielddeclarationsshouldbeatstartofclass
112+
m = @info_url.match(/pmd_rules_(\w+)_/)
113+
m[1]
114+
end
105115
end
106116
end

lib/pmdtester/runner.rb

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,11 @@ def clean
3333
def run_local_mode
3434
logger.info "Mode: #{@options.mode}"
3535
get_projects(@options.project_list) unless @options.nil?
36-
rule_sets = RuleSetBuilder.new(@options).build if @options.auto_config_flag
37-
return if rule_sets&.empty?
36+
if @options.auto_config_flag
37+
run_required = RuleSetBuilder.new(@options).build?
38+
logger.debug "Run required: #{run_required}"
39+
return unless run_required
40+
end
3841

3942
base_branch_details = create_pmd_report(config: @options.base_config, branch: @options.base_branch)
4043
patch_branch_details = create_pmd_report(config: @options.patch_config, branch: @options.patch_branch)
@@ -51,7 +54,8 @@ def run_online_mode
5154
get_projects(project_list)
5255

5356
if @options.auto_config_flag
54-
return if RuleSetBuilder.new(@options).build.empty?
57+
logger.info 'Autogenerating a dynamic ruleset based on source changes'
58+
return unless RuleSetBuilder.new(@options).build?
5559
elsif @options.patch_config == Options::DEFAULT_CONFIG_PATH
5660
# patch branch build pmd reports with same configuration as base branch
5761
# if not specified otherwise. This allows to use a different config (e.g. less rules)

0 commit comments

Comments
 (0)