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
@@ -8,92 +8,74 @@ module PmdTester
8
8
  # projects and branch of pmd source code
9
9
  class PmdReportBuilder
10
10
  include PmdTester
11
- def initialize(branch_config, projects, local_git_repo, pmd_branch_name, threads = 1)
12
- @branch_config = branch_config
11
+
12
+ def initialize(projects, options, branch_config, branch_name)
13
13
  @projects = projects
14
- @local_git_repo = local_git_repo
15
- @pmd_branch_name = pmd_branch_name
16
- @threads = threads
14
+ @local_git_repo = options.local_git_repo
15
+ @threads = options.threads
16
+ @error_recovery = options.error_recovery
17
+ @branch_config = branch_config
18
+ @pmd_branch_name = branch_name
17
19
  @pwd = Dir.getwd
18
20
 
19
- @pmd_branch_details = PmdBranchDetail.new(pmd_branch_name)
20
- end
21
-
22
- def execute_reset_cmd(type, tag)
23
- case type
24
- when 'git'
25
- reset_cmd = "git reset --hard #{tag}"
26
- when 'hg'
27
- reset_cmd = "hg up #{tag}"
28
- end
29
-
30
- Cmd.execute(reset_cmd)
31
- end
32
-
33
- def clone_projects
34
- logger.info 'Cloning projects started'
35
-
36
- @projects.each do |project|
37
- logger.info "Start cloning #{project.name} repository"
38
- path = project.local_source_path
39
- clone_cmd = "#{project.type} clone #{project.connection} #{path}"
40
- if File.exist?(path)
41
- logger.warn "Skipping clone, project path #{path} already exists"
42
- else
43
- Cmd.execute(clone_cmd)
44
- end
45
-
46
- Dir.chdir(path) do
47
- execute_reset_cmd(project.type, project.tag)
48
- end
49
- logger.info "Cloning #{project.name} completed"
50
- end
51
-
52
- logger.info 'Cloning projects completed'
21
+ @pmd_branch_details = PmdBranchDetail.new(@pmd_branch_name)
22
+ @project_builder = ProjectBuilder.new(@projects)
53
23
  end
54
24
 
55
25
  def get_pmd_binary_file
56
26
  logger.info "#{@pmd_branch_name}: Start packaging PMD"
57
27
  Dir.chdir(@local_git_repo) do
58
- current_head_sha = Cmd.execute('git rev-parse HEAD')
59
- current_branch_sha = Cmd.execute("git rev-parse #{@pmd_branch_name}")
28
+ build_branch_sha = Cmd.execute("git rev-parse #{@pmd_branch_name}^{commit}")
29
+
30
+ checkout_build_branch # needs a clean working tree, otherwise fails
60
31
 
61
- @pmd_version = determine_pmd_version
32
+ raise "Wrong branch #{get_last_commit_sha}" unless build_branch_sha == get_last_commit_sha
62
33
 
63
- # in case we are already on the correct branch
64
- # and a binary zip already exists...
65
- if current_head_sha == current_branch_sha &&
66
- File.exist?("pmd-dist/target/pmd-bin-#{@pmd_version}.zip")
67
- logger.warn "#{@pmd_branch_name}: Skipping packaging - zip for " \
68
- "#{@pmd_version} already exists"
34
+ distro_path = saved_distro_path(build_branch_sha)
35
+ logger.debug "#{@pmd_branch_name}: PMD Version is #{@pmd_version} " \
36
+ "(sha=#{build_branch_sha})"
37
+ logger.debug "#{@pmd_branch_name}: distro_path=#{distro_path}"
38
+ if File.directory?(distro_path)
39
+ logger.info "#{@pmd_branch_name}: Skipping packaging - saved version exists " \
40
+ " in #{distro_path}"
69
41
  else
70
- build_pmd
42
+ build_pmd(into_dir: distro_path)
71
43
  end
72
44
 
73
- @pmd_branch_details.branch_last_sha = get_last_commit_sha
45
+ # we're still on the build branch
46
+ @pmd_branch_details.branch_last_sha = build_branch_sha
74
47
  @pmd_branch_details.branch_last_message = get_last_commit_message
75
-
76
- logger.info "#{@pmd_branch_name}: Extracting the zip"
77
- unzip_cmd = "unzip -qo pmd-dist/target/pmd-bin-#{@pmd_version}.zip -d #{@pwd}/target"
78
- Cmd.execute(unzip_cmd)
79
48
  end
80
49
  logger.info "#{@pmd_branch_name}: Packaging PMD completed"
81
50
  end
82
51
 
83
- def build_pmd
84
- logger.info "#{@pmd_branch_name}: Checking out the branch"
85
- checkout_cmd = "git checkout #{@pmd_branch_name}"
86
- Cmd.execute(checkout_cmd)
87
-
88
- # determine the version again - it might be different in the other branch
89
- @pmd_version = determine_pmd_version
52
+ # builds pmd on currently checked out branch
53
+ def build_pmd(into_dir:)
54
+ # in CI there might have been a build performed already. In that case
55
+ # we reuse the build result, otherwise we build PMD freshly
56
+ pmd_dist_target = "pmd-dist/target/pmd-bin-#{@pmd_version}.zip"
57
+ binary_exists = File.exist?(pmd_dist_target)
58
+ logger.debug "#{@pmd_branch_name}: Does the file #{pmd_dist_target} exist? #{binary_exists} (cwd: #{Dir.getwd})"
59
+ if binary_exists
60
+ # that's a warning, because we don't know, whether this build really
61
+ # belongs to the current branch or whether it's from a previous branch.
62
+ # In CI, that's not a problem, because the workspace is always fresh.
63
+ logger.warn "#{@pmd_branch_name}: Reusing already existing #{pmd_dist_target}"
64
+ else
65
+ logger.info "#{@pmd_branch_name}: Building PMD #{@pmd_version}..."
66
+ package_cmd = './mvnw clean package' \
67
+ ' -Dmaven.test.skip=true' \
68
+ ' -Dmaven.javadoc.skip=true' \
69
+ ' -Dmaven.source.skip=true' \
70
+ ' -Dcheckstyle.skip=true' \
71
+ ' -Dpmd.skip=true' \
72
+ ' -T1C -B'
73
+ Cmd.execute(package_cmd)
74
+ end
90
75
 
91
- logger.info "#{@pmd_branch_name}: Building PMD #{@pmd_version}..."
92
- package_cmd = './mvnw clean package' \
93
- ' -Dmaven.test.skip=true' \
94
- ' -Dmaven.javadoc.skip=true' \
95
- ' -Dmaven.source.skip=true'
96
- Cmd.execute(package_cmd)
76
+ logger.info "#{@pmd_branch_name}: Extracting the zip"
77
+ Cmd.execute("unzip -qo #{pmd_dist_target} -d pmd-dist/target/exploded")
78
+ Cmd.execute("mv pmd-dist/target/exploded/pmd-bin-#{@pmd_version} #{into_dir}")
97
79
  end
98
80
 
99
81
  def determine_pmd_version
@@ -103,7 +85,7 @@ module PmdTester
103
85
  end
104
86
 
105
87
  def get_last_commit_sha
106
- get_last_commit_sha_cmd = 'git rev-parse HEAD'
88
+ get_last_commit_sha_cmd = 'git rev-parse HEAD^{commit}'
107
89
  Cmd.execute(get_last_commit_sha_cmd)
108
90
  end
109
91
 
@@ -113,27 +95,39 @@ module PmdTester
113
95
  end
114
96
 
115
97
  def generate_pmd_report(project)
116
- run_path = "target/pmd-bin-#{@pmd_version}/bin/run.sh"
117
- pmd_cmd = "#{run_path} pmd -d #{project.local_source_path} -f xml " \
98
+ error_recovery_options = @error_recovery ? 'PMD_JAVA_OPTS="-Dpmd.error_recovery -ea" ' : ''
99
+ run_path = "#{saved_distro_path(@pmd_branch_details.branch_last_sha)}/bin/run.sh"
100
+ pmd_cmd = "#{error_recovery_options}" \
101
+ "#{run_path} pmd -d #{project.local_source_path} -f xml " \
118
102
  "-R #{project.get_config_path(@pmd_branch_name)} " \
119
103
  "-r #{project.get_pmd_report_path(@pmd_branch_name)} " \
120
- "-failOnViolation false -t #{@threads}"
104
+ "-failOnViolation false -t #{@threads} " \
105
+ "#{project.auxclasspath}"
121
106
  start_time = Time.now
122
- Cmd.execute(pmd_cmd)
107
+ if File.exist?(project.get_pmd_report_path(@pmd_branch_name))
108
+ logger.warn "#{@pmd_branch_name}: Skipping PMD run - report " \
109
+ "#{project.get_pmd_report_path(@pmd_branch_name)} already exists"
110
+ else
111
+ Cmd.execute(pmd_cmd)
112
+ end
123
113
  end_time = Time.now
124
114
  [end_time - start_time, end_time]
125
115
  end
126
116
 
127
117
  def generate_config_for(project)
118
+ logger.debug "Generating ruleset with excludes from #{@branch_config}"
128
119
  doc = Nokogiri::XML(File.read(@branch_config))
129
120
  ruleset = doc.at_css('ruleset')
130
- project.exclude_pattern.each do |exclude_pattern|
131
- ruleset.add_child("<exclude-pattern>#{exclude_pattern}</exclude-pattern>")
121
+ ruleset.add_child("\n")
122
+ project.exclude_patterns.each do |exclude_pattern|
123
+ ruleset.add_child(" <exclude-pattern>#{exclude_pattern}</exclude-pattern>\n")
132
124
  end
133
125
 
134
126
  File.open(project.get_config_path(@pmd_branch_name), 'w') do |x|
135
127
  x << doc.to_s
136
128
  end
129
+
130
+ logger.debug "Created file #{project.get_config_path(@pmd_branch_name)}"
137
131
  end
138
132
 
139
133
  def generate_pmd_reports
@@ -141,16 +135,14 @@ module PmdTester
141
135
 
142
136
  sum_time = 0
143
137
  @projects.each do |project|
144
- progress_logger = SimpleProgressLogger.new(project.name)
138
+ progress_logger = SimpleProgressLogger.new("generating #{project.name}'s PMD report")
145
139
  progress_logger.start
146
140
  generate_config_for(project)
147
141
  execution_time, end_time = generate_pmd_report(project)
148
142
  progress_logger.stop
149
143
  sum_time += execution_time
150
144
 
151
- report_details = PmdReportDetail.new
152
- report_details.execution_time = execution_time
153
- report_details.timestamp = end_time
145
+ report_details = PmdReportDetail.new(execution_time: execution_time, timestamp: end_time)
154
146
  report_details.save(project.get_report_info_path(@pmd_branch_name))
155
147
  logger.info "#{project.name}'s PMD report was generated successfully"
156
148
  end
@@ -161,10 +153,46 @@ module PmdTester
161
153
  @pmd_branch_details
162
154
  end
163
155
 
156
+ # returns the branch details
164
157
  def build
165
- clone_projects
158
+ @project_builder.clone_projects
159
+ @project_builder.build_projects
166
160
  get_pmd_binary_file
167
161
  generate_pmd_reports
168
162
  end
163
+
164
+ private
165
+
166
+ def checkout_build_branch
167
+ logger.info "#{@pmd_branch_name}: Checking out the branch"
168
+ # note that this would fail if the tree is dirty
169
+ Cmd.execute("git checkout #{@pmd_branch_name}")
170
+
171
+ # determine the version
172
+ @pmd_version = determine_pmd_version
173
+
174
+ return unless wd_has_dirty_git_changes
175
+
176
+ # working dir is dirty....
177
+ # we don't allow this because we need the SHA to address the zip file
178
+ logger.error "#{@pmd_branch_name}: Won\'t build without a clean working tree, " \
179
+ 'commit your changes'
180
+ end
181
+
182
+ def work_dir
183
+ "#{@pwd}/target"
184
+ end
185
+
186
+ # path to the unzipped distribution
187
+ # e.g. <cwd>/pmd-bin-<version>-<branch>-<sha>
188
+ def saved_distro_path(build_sha)
189
+ "#{work_dir}/pmd-bin-#{@pmd_version}" \
190
+ "-#{PmdBranchDetail.branch_filename(@pmd_branch_name)}" \
191
+ "-#{build_sha}"
192
+ end
193
+
194
+ def wd_has_dirty_git_changes
195
+ !Cmd.execute('git status --porcelain').empty?
196
+ end
169
197
  end
170
198
  end
@@ -0,0 +1,105 @@
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.clone_root_path
21
+
22
+ if File.exist?(path)
23
+ logger.warn "Skipping clone, project path #{path} already exists"
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
+
33
+ Cmd.execute(clone_cmd)
34
+ end
35
+
36
+ Dir.chdir(path) do
37
+ execute_reset_cmd(project.type, project.tag)
38
+ end
39
+ logger.info "Cloning #{project.name} completed"
40
+ end
41
+
42
+ logger.info 'Cloning projects completed'
43
+ end
44
+
45
+ def build_projects
46
+ logger.info 'Building projects started'
47
+
48
+ @projects.each do |project|
49
+ path = project.clone_root_path
50
+ Dir.chdir(path) do
51
+ progress_logger = SimpleProgressLogger.new("building #{project.name} in #{path}")
52
+ progress_logger.start
53
+ prepare_project(project)
54
+ progress_logger.stop
55
+ end
56
+ logger.info "Building #{project.name} completed"
57
+ end
58
+
59
+ logger.info 'Building projects completed'
60
+ end
61
+
62
+ private
63
+
64
+ def prepare_project(project)
65
+ # Note: current working directory is the project directory,
66
+ # where the source code has been cloned to
67
+ if project.build_command
68
+ logger.debug "Executing build-command: #{project.build_command}"
69
+ run_as_script(Dir.getwd, project.build_command)
70
+ end
71
+ if project.auxclasspath_command
72
+ logger.debug "Executing auxclasspath-command: #{project.auxclasspath_command}"
73
+ auxclasspath = run_as_script(Dir.getwd, project.auxclasspath_command)
74
+ project.auxclasspath = "-auxclasspath #{auxclasspath}"
75
+ else
76
+ project.auxclasspath = ''
77
+ end
78
+ end
79
+
80
+ def run_as_script(path, command)
81
+ script = Tempfile.new(['pmd-regression-', '.sh'], path)
82
+ logger.debug "Creating script #{script.path}"
83
+ begin
84
+ script.write(command)
85
+ script.close
86
+ shell = 'sh -xe'
87
+ if command.start_with?('#!')
88
+ shell = command.lines[0].chomp[2..] # remove leading "#!"
89
+ end
90
+ stdout = Cmd.execute("#{shell} #{script.path}")
91
+ ensure
92
+ script.unlink
93
+ end
94
+ stdout
95
+ end
96
+
97
+ def execute_reset_cmd(type, tag)
98
+ raise "Unsupported project type '#{type}' - only git is supported" unless type == 'git'
99
+
100
+ reset_cmd = "git checkout #{tag}; git reset --hard #{tag}"
101
+
102
+ Cmd.execute(reset_cmd)
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,128 @@
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 violations_to_hash(project, violations_by_file, is_diff)
29
+ filename_index = []
30
+ all_vs = []
31
+ violations_by_file.each do |file, vs|
32
+ file_ref = filename_index.size
33
+ filename_index.push(project.get_local_path(file))
34
+ vs.each do |v|
35
+ all_vs.push(make_violation_hash(file_ref, v, is_diff))
36
+ end
37
+ end
38
+
39
+ {
40
+ 'file_index' => filename_index,
41
+ 'violations' => all_vs
42
+ }
43
+ end
44
+
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) }
48
+ end
49
+
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) }
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 error_to_hash(error, project)
61
+ escaped_stacktrace = sanitize_stacktrace(error)
62
+ old_stacktrace = error.old_error.nil? ? nil : sanitize_stacktrace(error.old_error)
63
+
64
+ {
65
+ 'file_url' => project.get_webview_url(error.filename),
66
+ 'stack_trace_html' => escaped_stacktrace,
67
+ 'old_stack_trace_html' => old_stacktrace,
68
+ 'short_message' => error.short_message,
69
+ 'short_filename' => error.short_filename,
70
+ 'filename' => error.filename,
71
+ 'change_type' => change_type(error)
72
+ }
73
+ end
74
+
75
+ def sanitize_stacktrace(error)
76
+ CGI.escapeHTML(error.stack_trace)
77
+ .gsub(error.filename, '<span class="meta-var">$FILE</span>')
78
+ .gsub(/\w++(?=\(\w++\.java:\d++\))/, '<span class="stack-trace-method">\\0</span>')
79
+ end
80
+
81
+ def configerror_to_hash(configerror)
82
+ {
83
+ 'rule' => configerror.rulename,
84
+ 'message' => configerror.msg,
85
+ 'change_type' => change_type(configerror)
86
+ }
87
+ end
88
+
89
+ def change_type(item)
90
+ if item.branch == BASE
91
+ 'removed'
92
+ elsif item.changed?
93
+ 'changed'
94
+ else
95
+ 'added'
96
+ end
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
127
+ end
128
+ end