rubocop-metz 0.5.2
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.
Potentially problematic release.
This version of rubocop-metz might be problematic. Click here for more details.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/lib/metz_scan/analyzers/contextual_node_walker.rb +82 -0
- data/lib/metz_scan/analyzers/cross_package_reference_metadata.rb +36 -0
- data/lib/metz_scan/analyzers/cross_package_reference_set.rb +33 -0
- data/lib/metz_scan/analyzers/implicit_context_pressure/ambient_context_collector.rb +54 -0
- data/lib/metz_scan/analyzers/implicit_context_pressure/ambient_context_reference.rb +37 -0
- data/lib/metz_scan/analyzers/implicit_context_pressure/current_attribute_collector.rb +73 -0
- data/lib/metz_scan/analyzers/implicit_context_pressure/reference_set.rb +37 -0
- data/lib/metz_scan/analyzers/implicit_context_pressure/thread_current_collector.rb +76 -0
- data/lib/metz_scan/analyzers/implicit_context_pressure/triage.rb +109 -0
- data/lib/metz_scan/analyzers/implicit_context_pressure.rb +139 -0
- data/lib/metz_scan/analyzers/inheritance_descendants/finding.rb +18 -0
- data/lib/metz_scan/analyzers/inheritance_descendants/metadata.rb +23 -0
- data/lib/metz_scan/analyzers/inheritance_descendants/root_kind.rb +58 -0
- data/lib/metz_scan/analyzers/inheritance_descendants/triage.rb +30 -0
- data/lib/metz_scan/analyzers/inheritance_descendants.rb +137 -0
- data/lib/metz_scan/analyzers/namespace_leak_pressure/finding.rb +20 -0
- data/lib/metz_scan/analyzers/namespace_leak_pressure/metadata.rb +40 -0
- data/lib/metz_scan/analyzers/namespace_leak_pressure/namespace.rb +41 -0
- data/lib/metz_scan/analyzers/namespace_leak_pressure/reference_collector.rb +50 -0
- data/lib/metz_scan/analyzers/namespace_leak_pressure/reference_set.rb +12 -0
- data/lib/metz_scan/analyzers/namespace_leak_pressure/shared_namespace_triage/exception_declaration.rb +117 -0
- data/lib/metz_scan/analyzers/namespace_leak_pressure/shared_namespace_triage.rb +69 -0
- data/lib/metz_scan/analyzers/namespace_leak_pressure.rb +139 -0
- data/lib/metz_scan/analyzers/occurrence.rb +28 -0
- data/lib/metz_scan/analyzers/package_dependency_pressure/finding.rb +20 -0
- data/lib/metz_scan/analyzers/package_dependency_pressure/metadata.rb +27 -0
- data/lib/metz_scan/analyzers/package_dependency_pressure/package_map.rb +11 -0
- data/lib/metz_scan/analyzers/package_dependency_pressure/reference_collector.rb +38 -0
- data/lib/metz_scan/analyzers/package_dependency_pressure/reference_set.rb +12 -0
- data/lib/metz_scan/analyzers/package_dependency_pressure/shared_dependency_triage.rb +124 -0
- data/lib/metz_scan/analyzers/package_dependency_pressure.rb +139 -0
- data/lib/metz_scan/analyzers/package_map.rb +81 -0
- data/lib/metz_scan/analyzers/project_analyzer_triage.rb +20 -0
- data/lib/metz_scan/analyzers/repeated_branching/branch_site_collector.rb +138 -0
- data/lib/metz_scan/analyzers/repeated_branching/branch_value.rb +27 -0
- data/lib/metz_scan/analyzers/repeated_branching/contextual_node_walker.rb +11 -0
- data/lib/metz_scan/analyzers/repeated_branching/decision_subject.rb +46 -0
- data/lib/metz_scan/analyzers/repeated_branching/predicate_chain.rb +58 -0
- data/lib/metz_scan/analyzers/repeated_branching/triage.rb +28 -0
- data/lib/metz_scan/analyzers/repeated_branching.rb +135 -0
- data/lib/metz_scan/analyzers/repeated_query_criteria/triage.rb +103 -0
- data/lib/metz_scan/analyzers/repeated_query_criteria.rb +285 -0
- data/lib/metz_scan/analyzers/ruby_file_enumerator.rb +37 -0
- data/lib/metz_scan/analyzers/service_soup/service_call_pattern.rb +53 -0
- data/lib/metz_scan/analyzers/service_soup/setup_workflow_triage.rb +48 -0
- data/lib/metz_scan/analyzers/service_soup/workflow_collector.rb +130 -0
- data/lib/metz_scan/analyzers/service_soup.rb +128 -0
- data/lib/metz_scan/analyzers/subclass_override_pressure/family_builder.rb +69 -0
- data/lib/metz_scan/analyzers/subclass_override_pressure/finding.rb +30 -0
- data/lib/metz_scan/analyzers/subclass_override_pressure/metadata.rb +74 -0
- data/lib/metz_scan/analyzers/subclass_override_pressure/method_body_facts.rb +100 -0
- data/lib/metz_scan/analyzers/subclass_override_pressure/triage.rb +106 -0
- data/lib/metz_scan/analyzers/subclass_override_pressure.rb +133 -0
- data/lib/metz_scan/calibration/project_analyzer_evidence_runner/artifact_writer.rb +51 -0
- data/lib/metz_scan/calibration/project_analyzer_evidence_runner/baseline_delta.rb +231 -0
- data/lib/metz_scan/calibration/project_analyzer_evidence_runner/collaborators.rb +76 -0
- data/lib/metz_scan/calibration/project_analyzer_evidence_runner/markdown_renderer.rb +353 -0
- data/lib/metz_scan/calibration/project_analyzer_evidence_runner/readiness_catalog.rb +123 -0
- data/lib/metz_scan/calibration/project_analyzer_evidence_runner/summary.rb +333 -0
- data/lib/metz_scan/calibration/project_analyzer_evidence_runner/target_set.rb +139 -0
- data/lib/metz_scan/calibration/project_analyzer_evidence_runner.rb +64 -0
- data/lib/metz_scan/calibration/rubydex_drift_formatter.rb +91 -0
- data/lib/metz_scan/cli.rb +120 -0
- data/lib/metz_scan/commands/explain.rb +128 -0
- data/lib/metz_scan/commands/project_analyzers.rb +105 -0
- data/lib/metz_scan/commands/report.rb +115 -0
- data/lib/metz_scan/commands/rules.rb +92 -0
- data/lib/metz_scan/commands/scan/auto_fix.rb +138 -0
- data/lib/metz_scan/commands/scan/compliance_scorecard.rb +124 -0
- data/lib/metz_scan/commands/scan/github_annotations_renderer.rb +55 -0
- data/lib/metz_scan/commands/scan/offense_extractor.rb +42 -0
- data/lib/metz_scan/commands/scan/project_analyzer_breakdown.rb +55 -0
- data/lib/metz_scan/commands/scan/project_analyzer_metadata.rb +78 -0
- data/lib/metz_scan/commands/scan/project_analyzer_offenses.rb +85 -0
- data/lib/metz_scan/commands/scan/project_analyzer_runner.rb +148 -0
- data/lib/metz_scan/commands/scan/project_analyzer_summary_aggregate_formatter.rb +74 -0
- data/lib/metz_scan/commands/scan/project_analyzer_summary_breakdown_formatter.rb +46 -0
- data/lib/metz_scan/commands/scan/project_analyzer_triage_formatter.rb +40 -0
- data/lib/metz_scan/commands/scan/project_analyzer_triage_priority.rb +38 -0
- data/lib/metz_scan/commands/scan/project_config_scope.rb +231 -0
- data/lib/metz_scan/commands/scan/runner.rb +199 -0
- data/lib/metz_scan/commands/scan/sarif_renderer.rb +103 -0
- data/lib/metz_scan/commands/scan/sarif_rule_descriptors.rb +82 -0
- data/lib/metz_scan/commands/scan/sarif_severity.rb +27 -0
- data/lib/metz_scan/commands/scan/target_ruby_version.rb +56 -0
- data/lib/metz_scan/commands/scan/text_renderer.rb +145 -0
- data/lib/metz_scan/commands/scan.rb +201 -0
- data/lib/metz_scan/project_index/null_backend.rb +35 -0
- data/lib/metz_scan/project_index/rubydex_backend/file_discovery.rb +33 -0
- data/lib/metz_scan/project_index/rubydex_backend/location_formatting.rb +36 -0
- data/lib/metz_scan/project_index/rubydex_backend/method_declarations.rb +118 -0
- data/lib/metz_scan/project_index/rubydex_backend.rb +133 -0
- data/lib/metz_scan/project_index.rb +109 -0
- data/lib/metz_scan/version.rb +5 -0
- data/lib/metz_scan.rb +5 -0
- metadata +185 -0
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "open3"
|
|
4
|
+
require "stringio"
|
|
5
|
+
require "tempfile"
|
|
6
|
+
|
|
7
|
+
module MetzScan
|
|
8
|
+
module Commands
|
|
9
|
+
class Scan
|
|
10
|
+
class AutoFix
|
|
11
|
+
SAFE_FLAG = "-a"
|
|
12
|
+
UNSAFE_FLAG = "-A"
|
|
13
|
+
CommandResult = Struct.new(:status, :stdout, :stderr, keyword_init: true)
|
|
14
|
+
|
|
15
|
+
def initialize(stdout:, stderr:)
|
|
16
|
+
@stdout = stdout
|
|
17
|
+
@stderr = stderr
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def run(options)
|
|
21
|
+
require "rubocop-metz"
|
|
22
|
+
options.dry_run ? run_dry(options) : run_in_place(options)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
attr_reader :stdout, :stderr
|
|
28
|
+
|
|
29
|
+
def run_in_place(options)
|
|
30
|
+
with_redirected_io { RuboCop::CLI.new.run(rubocop_argv(options)) }
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def with_redirected_io
|
|
34
|
+
previous = redirect_to(stdout, stderr)
|
|
35
|
+
yield
|
|
36
|
+
ensure
|
|
37
|
+
restore_io(previous) if previous
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def redirect_to(out, err)
|
|
41
|
+
previous = [$stdout, $stderr]
|
|
42
|
+
$stdout = out
|
|
43
|
+
$stderr = err
|
|
44
|
+
previous
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def restore_io(previous)
|
|
48
|
+
$stdout = previous[0]
|
|
49
|
+
$stderr = previous[1]
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def run_dry(options)
|
|
53
|
+
snapshot = capture_files(options.paths)
|
|
54
|
+
dry_run_result(snapshot, capture_rubocop(rubocop_argv(options)))
|
|
55
|
+
ensure
|
|
56
|
+
restore_files(snapshot)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def dry_run_result(snapshot, result)
|
|
60
|
+
print_diffs(snapshot)
|
|
61
|
+
print_rubocop_failure(result) unless result.status.zero?
|
|
62
|
+
result.status
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def rubocop_argv(options)
|
|
66
|
+
flag = options.unsafe ? UNSAFE_FLAG : SAFE_FLAG
|
|
67
|
+
["--plugin", "rubocop-metz", *Runner.cop_selection_argv(options.all_cops), flag, *options.paths]
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def capture_rubocop(argv)
|
|
71
|
+
out = StringIO.new
|
|
72
|
+
err = StringIO.new
|
|
73
|
+
status = with_captured_io(out, err) { RuboCop::CLI.new.run(argv) }
|
|
74
|
+
CommandResult.new(status: status, stdout: out.string, stderr: err.string)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def with_captured_io(out, err)
|
|
78
|
+
previous = redirect_to(out, err)
|
|
79
|
+
yield
|
|
80
|
+
ensure
|
|
81
|
+
restore_io(previous) if previous
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def capture_files(paths)
|
|
85
|
+
files_under(paths).to_h { |path| [path, File.binread(path)] }
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def files_under(paths)
|
|
89
|
+
paths.flat_map { |path| File.directory?(path) ? regular_files_in(path) : [path] }
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def regular_files_in(dir)
|
|
93
|
+
Dir.glob(File.join(dir, "**/*")).select { |entry| File.file?(entry) }
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def print_diffs(snapshot)
|
|
97
|
+
snapshot.each { |path, original| print_file_diff(path, original) }
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def print_rubocop_failure(result)
|
|
101
|
+
stderr.print(result.stderr) unless result.stderr.empty?
|
|
102
|
+
stderr.print(result.stdout) unless result.stdout.empty?
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def print_file_diff(path, original)
|
|
106
|
+
return unless File.exist?(path)
|
|
107
|
+
return if File.binread(path) == original
|
|
108
|
+
|
|
109
|
+
stdout.puts diff_against(path, original)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def diff_against(path, original)
|
|
113
|
+
Tempfile.create(["metz-orig-", File.basename(path)]) do |orig|
|
|
114
|
+
write_tempfile(orig, original)
|
|
115
|
+
run_diff(orig.path, path)
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def write_tempfile(file, content)
|
|
120
|
+
file.binmode
|
|
121
|
+
file.write(content)
|
|
122
|
+
file.flush
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def run_diff(orig_path, new_path)
|
|
126
|
+
labels = ["--label", "a/#{new_path}", "--label", "b/#{new_path}"]
|
|
127
|
+
Open3.capture3("diff", "-u", *labels, orig_path, new_path).first
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def restore_files(snapshot)
|
|
131
|
+
return unless snapshot
|
|
132
|
+
|
|
133
|
+
snapshot.each { |path, content| File.binwrite(path, content) }
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MetzScan
|
|
4
|
+
module Commands
|
|
5
|
+
class Scan
|
|
6
|
+
class ComplianceScorecard
|
|
7
|
+
RULE_COP_PREFIX = "Metz/"
|
|
8
|
+
TOP_FILE_LIMIT = 5
|
|
9
|
+
|
|
10
|
+
def self.add_to_summary!(parsed)
|
|
11
|
+
new(parsed).add_to_summary!
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def initialize(parsed)
|
|
15
|
+
@parsed = parsed
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def add_to_summary!
|
|
19
|
+
writable_summary["clean_file_count"] = clean_file_count
|
|
20
|
+
writable_summary["files_with_offenses"] = files_with_rule_offenses
|
|
21
|
+
writable_summary["offenses_by_cop"] = offenses_by_cop
|
|
22
|
+
parsed
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def lines
|
|
26
|
+
["Summary", "-------", compliance_line, *offense_summary_lines]
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
attr_reader :parsed
|
|
32
|
+
|
|
33
|
+
def offense_summary_lines
|
|
34
|
+
return ["No offenses found."] if offense_count.zero?
|
|
35
|
+
|
|
36
|
+
[offense_total_line, "", "By cop:", *formatted_rows(sorted_cop_counts), "",
|
|
37
|
+
"Most offenses:", *formatted_rows(top_file_counts)]
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def compliance_line
|
|
41
|
+
return "Metz compliance: n/a (no files scanned)" if inspected_file_count.zero?
|
|
42
|
+
|
|
43
|
+
"Metz compliance: #{compliance_percentage}% (#{clean_file_count}/#{inspected_file_count} files clean)"
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def compliance_percentage
|
|
47
|
+
((clean_file_count.to_f / inspected_file_count) * 100).round
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def clean_file_count
|
|
51
|
+
inspected_file_count - files_with_rule_offenses
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def files_with_rule_offenses
|
|
55
|
+
files.count { |file| rule_offenses?(file) }
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def rule_offenses?(file)
|
|
59
|
+
Array(file["offenses"]).any? { |offense| offense.fetch("cop_name").start_with?(RULE_COP_PREFIX) }
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def offense_total_line
|
|
63
|
+
"#{count_label(offense_count, 'offense')} across #{count_label(sorted_cop_counts.size, 'cop')}"
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def offense_count
|
|
67
|
+
offenses_by_cop.values.sum
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def offenses_by_cop
|
|
71
|
+
@offenses_by_cop ||= sorted_cop_counts.to_h
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def sorted_cop_counts
|
|
75
|
+
@sorted_cop_counts ||= cop_counts.sort_by { |cop_name, count| [-count, cop_name] }
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def cop_counts
|
|
79
|
+
offenses.each_with_object(Hash.new(0)) { |offense, counts| counts[offense.fetch("cop_name")] += 1 }
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def top_file_counts
|
|
83
|
+
file_counts.sort_by { |path, count| [-count, path] }.first(TOP_FILE_LIMIT)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def file_counts
|
|
87
|
+
files.filter_map do |file|
|
|
88
|
+
count = Array(file["offenses"]).size
|
|
89
|
+
[file.fetch("path"), count] if count.positive?
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def formatted_rows(rows)
|
|
94
|
+
width = rows.map { |label, _count| label.length }.max
|
|
95
|
+
rows.map { |label, count| " #{label.ljust(width)} #{count}" }
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def count_label(count, noun)
|
|
99
|
+
"#{count} #{noun}#{'s' unless count == 1}"
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def offenses
|
|
103
|
+
@offenses ||= files.flat_map { |file| Array(file["offenses"]) }
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def files
|
|
107
|
+
@files ||= Array(parsed["files"])
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def inspected_file_count
|
|
111
|
+
@inspected_file_count ||= summary.fetch("inspected_file_count", files.size).to_i
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def summary
|
|
115
|
+
@summary ||= parsed.fetch("summary", {}) || {}
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def writable_summary
|
|
119
|
+
parsed["summary"] ||= {}
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "offense_extractor"
|
|
4
|
+
require_relative "project_analyzer_triage_formatter"
|
|
5
|
+
|
|
6
|
+
module MetzScan
|
|
7
|
+
module Commands
|
|
8
|
+
class Scan
|
|
9
|
+
class GithubAnnotationsRenderer
|
|
10
|
+
ERROR_SEVERITIES = %w[error fatal].freeze
|
|
11
|
+
|
|
12
|
+
def initialize(stdout, parsed)
|
|
13
|
+
@stdout = stdout
|
|
14
|
+
@parsed = parsed
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def render
|
|
18
|
+
OffenseExtractor.offenses(@parsed).each { |offense| stdout.puts annotation_for(offense) }
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
attr_reader :stdout
|
|
24
|
+
|
|
25
|
+
def annotation_for(offense)
|
|
26
|
+
"::#{level_for(offense)} #{properties_for(offense)}::#{escape_data(message_for(offense))}"
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def level_for(offense)
|
|
30
|
+
ERROR_SEVERITIES.include?(offense[:severity].to_s) ? "error" : "warning"
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def properties_for(offense)
|
|
34
|
+
annotation_properties(offense).map { |key, value| "#{key}=#{escape_property(value)}" }.join(",")
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def annotation_properties(offense)
|
|
38
|
+
{ file: offense[:path], line: offense[:line], col: offense[:column], title: offense[:cop_name] }.compact
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def message_for(offense)
|
|
42
|
+
[offense[:message], ProjectAnalyzerTriageFormatter.line(offense[:project_analyzer])].compact.join("\n")
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def escape_property(value)
|
|
46
|
+
escape_data(value).gsub(":", "%3A").gsub(",", "%2C")
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def escape_data(value)
|
|
50
|
+
value.to_s.gsub("%", "%25").gsub("\r", "%0D").gsub("\n", "%0A")
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MetzScan
|
|
4
|
+
module Commands
|
|
5
|
+
class Scan
|
|
6
|
+
module OffenseExtractor
|
|
7
|
+
module_function
|
|
8
|
+
|
|
9
|
+
def offenses(parsed)
|
|
10
|
+
Array(parsed["files"]).flat_map { |file| file_offenses(file) }
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def file_offenses(file)
|
|
14
|
+
Array(file["offenses"]).map { |o| offense_struct(file["path"], o) }
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def offense_struct(path, offense)
|
|
18
|
+
loc = offense["location"] || {}
|
|
19
|
+
base_offense(path, offense).merge(location_fields(loc))
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def base_offense(path, offense)
|
|
23
|
+
{ path: path, cop_name: offense["cop_name"], severity: offense["severity"],
|
|
24
|
+
message: offense["message"], why_it_matters: offense["why_it_matters"],
|
|
25
|
+
project_analyzer: offense["project_analyzer"] }
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def location_fields(location)
|
|
29
|
+
{ line: location_value(location, "start_line", "line"),
|
|
30
|
+
column: location_value(location, "start_column", "column") }
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def location_value(location, primary_key, fallback_key)
|
|
34
|
+
return location[primary_key] if location.key?(primary_key)
|
|
35
|
+
return location[fallback_key] if location.key?(fallback_key)
|
|
36
|
+
|
|
37
|
+
raise KeyError, "RuboCop JSON location missing #{primary_key}/#{fallback_key}"
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MetzScan
|
|
4
|
+
module Commands
|
|
5
|
+
class Scan
|
|
6
|
+
class ProjectAnalyzerBreakdown
|
|
7
|
+
def initialize(findings)
|
|
8
|
+
@findings = findings
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def to_h
|
|
12
|
+
{ "cop_name" => breakdown_for(&:rule_id),
|
|
13
|
+
"confidence" => breakdown_for { |finding| optional_value(finding, :confidence) },
|
|
14
|
+
"triage_severity" => breakdown_for { |finding| optional_value(finding, :triage_severity) },
|
|
15
|
+
"metadata" => metadata_breakdowns }.compact.reject { |_key, values| values.empty? }
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
attr_reader :findings
|
|
21
|
+
|
|
22
|
+
def metadata_breakdowns
|
|
23
|
+
category_metadata_keys.to_h { |key| [key, metadata_breakdown_for(key)] }
|
|
24
|
+
.reject { |_key, values| values.empty? }
|
|
25
|
+
.then { |values| values unless values.empty? }
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def category_metadata_keys
|
|
29
|
+
ProjectAnalyzerMetadata.category_metadata_keys
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def metadata_breakdown_for(key)
|
|
33
|
+
breakdown_for { |finding| analyzer_metadata(finding)[key] }
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def analyzer_metadata(finding)
|
|
37
|
+
return {} unless finding.respond_to?(:project_analyzer_metadata)
|
|
38
|
+
|
|
39
|
+
finding.project_analyzer_metadata || {}
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def breakdown_for(&block)
|
|
43
|
+
findings.filter_map { |finding| block.call(finding) }
|
|
44
|
+
.tally
|
|
45
|
+
.sort_by { |value, _count| value.to_s }
|
|
46
|
+
.map { |value, count| { "value" => value, "finding_count" => count } }
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def optional_value(finding, name)
|
|
50
|
+
finding.public_send(name) if finding.respond_to?(name)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "project_analyzer_breakdown"
|
|
4
|
+
require_relative "project_analyzer_triage_priority"
|
|
5
|
+
|
|
6
|
+
module MetzScan
|
|
7
|
+
module Commands
|
|
8
|
+
class Scan
|
|
9
|
+
module ProjectAnalyzerMetadata
|
|
10
|
+
CATEGORY_METADATA_KEY = "project_analyzer_category"
|
|
11
|
+
|
|
12
|
+
module_function
|
|
13
|
+
|
|
14
|
+
def category_metadata_keys = [CATEGORY_METADATA_KEY]
|
|
15
|
+
|
|
16
|
+
def category_for(finding)
|
|
17
|
+
analyzer_metadata(finding)[CATEGORY_METADATA_KEY]
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def offense_metadata(finding)
|
|
21
|
+
triage_metadata(finding).merge(analyzer_metadata(finding))
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def summary(findings, offenses)
|
|
25
|
+
rules = rule_summaries(findings, offenses)
|
|
26
|
+
{ "finding_count" => findings.size,
|
|
27
|
+
"offense_count" => rules.sum { |rule| rule.fetch("offense_count") },
|
|
28
|
+
"rules" => rules }
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def triage_metadata(finding)
|
|
32
|
+
{ "status" => optional_value(finding, :project_analyzer_status),
|
|
33
|
+
"confidence" => optional_value(finding, :confidence),
|
|
34
|
+
"triage_severity" => optional_value(finding, :triage_severity),
|
|
35
|
+
"triage_summary" => optional_value(finding, :triage_summary) }.compact
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def analyzer_metadata(finding)
|
|
39
|
+
return {} unless finding.respond_to?(:project_analyzer_metadata)
|
|
40
|
+
|
|
41
|
+
finding.project_analyzer_metadata || {}
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def rule_summaries(findings, offenses)
|
|
45
|
+
findings.group_by(&:rule_id).sort.map do |rule_id, rule_findings|
|
|
46
|
+
rule_summary(rule_id, rule_findings, offenses)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def rule_summary(rule_id, findings, offenses)
|
|
51
|
+
summary_metadata(findings).merge(rule_count_metadata(rule_id, findings, offenses),
|
|
52
|
+
"breakdowns" => ProjectAnalyzerBreakdown.new(findings).to_h)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def rule_count_metadata(rule_id, findings, offenses)
|
|
56
|
+
{ "cop_name" => rule_id, "finding_count" => findings.size,
|
|
57
|
+
"offense_count" => offense_count(rule_id, offenses) }
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def summary_metadata(findings)
|
|
61
|
+
triage_metadata(priority_finding(findings)).slice("status", "confidence", "triage_severity")
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def priority_finding(findings)
|
|
65
|
+
Array(findings).min_by { |finding| ProjectAnalyzerTriagePriority.sort_key(triage_metadata(finding)) }
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def offense_count(rule_id, offenses)
|
|
69
|
+
offenses.count { |offense| offense.fetch("cop_name") == rule_id }
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def optional_value(finding, name)
|
|
73
|
+
finding.public_send(name) if finding.respond_to?(name)
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "project_analyzer_metadata"
|
|
4
|
+
|
|
5
|
+
module MetzScan
|
|
6
|
+
module Commands
|
|
7
|
+
class Scan
|
|
8
|
+
module ProjectAnalyzerOffenses
|
|
9
|
+
OffenseSet = Struct.new(:by_path, :offenses, keyword_init: true)
|
|
10
|
+
|
|
11
|
+
module_function
|
|
12
|
+
|
|
13
|
+
def build(findings)
|
|
14
|
+
entries = entries_for(findings)
|
|
15
|
+
OffenseSet.new(by_path: offenses_by_path(entries), offenses: entries.map { |entry| entry.fetch(:offense) })
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def entries_for(findings)
|
|
19
|
+
findings.flat_map { |finding| offenses_for(finding) }
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def offenses_for(finding)
|
|
23
|
+
locations_for(finding).map do |location|
|
|
24
|
+
{ path: location.path, offense: offense_for(finding, location) }
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def locations_for(finding)
|
|
29
|
+
Array(finding.report_occurrences).compact.uniq
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def offense_for(finding, occurrence)
|
|
33
|
+
offense_metadata(finding, occurrence).merge("location" => location_hash(occurrence.report_line))
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def offense_metadata(finding, occurrence)
|
|
37
|
+
add_project_analyzer_metadata(common_offense_metadata(finding), finding, occurrence)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def common_offense_metadata(finding)
|
|
41
|
+
basic_offense_metadata(finding).merge(explanation_metadata(finding))
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def basic_offense_metadata(finding)
|
|
45
|
+
{ "cop_name" => finding.rule_id, "message" => finding.message,
|
|
46
|
+
"severity" => "refactor", "corrected" => false, "correctable" => false }
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def explanation_metadata(finding)
|
|
50
|
+
{ "why_it_matters" => finding.why_it_matters, "fix_safety" => "manual",
|
|
51
|
+
"suggested_next_moves" => suggested_next_moves_for(finding) }
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def add_project_analyzer_metadata(metadata, finding, occurrence)
|
|
55
|
+
project_metadata = ProjectAnalyzerMetadata.offense_metadata(finding)
|
|
56
|
+
return metadata if project_metadata.empty?
|
|
57
|
+
|
|
58
|
+
metadata.merge("project_analyzer" => project_metadata.merge(report_location_metadata(occurrence)))
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def report_location_metadata(occurrence)
|
|
62
|
+
{ "report_location" => { "line_source" => occurrence.line_source,
|
|
63
|
+
"context" => occurrence.context }.compact }
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def suggested_next_moves_for(finding)
|
|
67
|
+
return [] unless finding.respond_to?(:suggested_next_moves)
|
|
68
|
+
|
|
69
|
+
Array(finding.suggested_next_moves).map(&:to_s)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def location_hash(line)
|
|
73
|
+
line ||= 1
|
|
74
|
+
{ "start_line" => line, "start_column" => 1, "last_line" => line,
|
|
75
|
+
"last_column" => 1, "length" => 1, "line" => line, "column" => 1 }
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def offenses_by_path(entries)
|
|
79
|
+
entries.group_by { |entry| entry.fetch(:path) }
|
|
80
|
+
.transform_values { |grouped_entries| grouped_entries.map { |entry| entry.fetch(:offense) } }
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|